Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c457db59f | ||
|
|
169aa78479 | ||
|
|
123029d2e9 | ||
|
|
5402624e20 | ||
|
|
9bc5399b6c | ||
|
|
f6476c786d | ||
|
|
5f19488c47 | ||
|
|
37aa83ee45 | ||
|
|
261a2f952e | ||
|
|
4c64cbc168 | ||
|
|
6856634a69 | ||
|
|
6ccd4adaa5 | ||
|
|
bd84628ba3 | ||
|
|
0cade11843 | ||
|
|
c035c39833 | ||
|
|
e9332d5043 | ||
|
|
08a16cf5fa | ||
|
|
9027cd1e6f | ||
|
|
b039657b84 | ||
|
|
9bd3472b3c | ||
|
|
610e51fb2c | ||
|
|
c098c80ec8 | ||
|
|
248c9468e1 | ||
|
|
d97ca399f0 | ||
|
|
2ffbc2ec58 | ||
|
|
86b12c1386 | ||
|
|
3b0f097d8d | ||
|
|
ac0f15f685 | ||
|
|
f2c684620e | ||
|
|
59b9c284e5 | ||
|
|
1cd66fdc01 | ||
|
|
61d88fd7fc | ||
|
|
1d98f1464c | ||
|
|
d9887c3d70 | ||
|
|
1f11cd8e9d | ||
|
|
d2d9c10f6c | ||
|
|
2fd974c7e4 | ||
|
|
e556cc7bc9 | ||
|
|
77f48d8c93 | ||
|
|
8449c8ba0c | ||
|
|
6d9dc4b8ca | ||
|
|
2921d61b9d | ||
|
|
9ef1049464 | ||
|
|
9667bf1436 | ||
|
|
ba772163cc | ||
|
|
2b85fdffee | ||
|
|
5b5d32d97a | ||
|
|
b0ef76cb76 | ||
|
|
003b95af3e | ||
|
|
ec345572e5 | ||
|
|
4ef515a906 | ||
|
|
5e9d814e6f | ||
|
|
28cfb82ce4 | ||
|
|
347710cc8d | ||
|
|
5e45673597 | ||
|
|
88740a78a0 | ||
|
|
69c347f198 | ||
|
|
0acb02c333 | ||
|
|
9b0ad1b39c | ||
|
|
8ade518402 | ||
|
|
13e66742a1 | ||
|
|
2836c3f71d | ||
|
|
84d94f9e24 | ||
|
|
92176f614b | ||
|
|
93c6a1fc88 | ||
|
|
67c2684ad6 | ||
|
|
895ee81d51 |
@@ -12,12 +12,14 @@ KANBANTE_PUBLIC_URL=https://kanbante.dracodev.net
|
||||
DRACO_SSO_URL=https://og.dracodev.net
|
||||
DRACO_FLOWDECK_URL=https://flowdeck.dracodev.net
|
||||
DRACO_TIKTAK_URL=https://tiktak.dracodev.net
|
||||
DRACO_SHAARIT_URL=https://shaarit.dracodev.net
|
||||
AGENT_URL=https://agent.dracodev.net
|
||||
# Exemples dev (à décommenter sur l'hôte dev) :
|
||||
# KANBANTE_PUBLIC_URL=https://kanbante-dev.dracodev.net
|
||||
# DRACO_SSO_URL=https://og-dev.dracodev.net
|
||||
# DRACO_FLOWDECK_URL=https://flowdeck-dev.dracodev.net
|
||||
# DRACO_TIKTAK_URL=https://tiktak-dev.dracodev.net
|
||||
# DRACO_SHAARIT_URL=https://shaarit-dev.dracodev.net
|
||||
# AGENT_URL=https://agent-dev.dracodev.net
|
||||
# Journal d'événements du service d'agents (signé HMAC)
|
||||
# AGENT_EVENEMENTS_URL= # vide = dérivé de AGENT_URL (/api/v1/evenements)
|
||||
|
||||
@@ -1,3 +1,98 @@
|
||||
## 0.46.0 — 2026-11-06 — Correspondance draco par utilisateur (HUB-16, carte fusion #130)
|
||||
|
||||
Version **mineure** : c'est une fonction (nouveau comportement d'identité), selon le patron du dépôt (fonctions en mineur — 0.24.0 comptes, 0.25.0 jetons ; les 0.45.x étaient des vendanges et correctifs).
|
||||
|
||||
- **Fin de l'hypothèse mono-utilisateur du pont SSO (ACC-3.2)** : un jeton `draco_session` valide désigne désormais **le compte Kanbante de son identité**. La correspondance vit dans la colonne existante `users.draco_sub` (UNIQUE, depuis la 0.24.0) ; la résolution est centralisée dans `app.accounts.resolve_draco_user()`, seul point d'entrée du pont `/auth/draco` et du contrôle draco de `/api/search`.
|
||||
- **Création JIT** : à la première connexion draco d'une identité sans correspondance, son compte Kanbante est créé automatiquement — nom dérivé des claims (slug du nom affiché, suffixé en cas de collision, jamais d'adoption d'un compte non lié), rôle **membre**, mot de passe local aléatoire haché **non divulgué** (l'accès normal passe par draco), membre de l'espace Principal, geste journalisé (`draco.jit`).
|
||||
- **Compte d'amorçage** : si l'unique compte existant est un admin actif sans liaison, le premier `sub` valide s'y lie au premier passage (journalisé `draco.link.amorcage`) — le compte actuel de l'opérateur garde son accès ; ses voies natives (mot de passe du compte, repli environnement formulaire/Basic) sont inchangées.
|
||||
- **Liaison par l'administrateur** : colonne « Draco (sub) » de `/admin` — saisir le `sub` et « Lier », ou « Délier » ; refus si le `sub` est déjà lié à un autre compte ; permis sur son propre compte (voie manuelle pour l'amorçage). Journalisé (`draco.link.admin` / `draco.unlink.admin`). Procédure et modèle : `docs/correspondance-draco.md`. Le registre matriciel côté Hub viendra avec HUB-19 ; ici, seul le côté Kanbante — la source de vérité locale — est livré.
|
||||
- **`/api/search`** : le cookie draco n'y vaut plus « opérateur » — le `sub` doit désigner un compte (correspondance ou amorçage) ; aucun JIT sur cette surface.
|
||||
- **Non-régressions** : jetons personnels `kbt_` (création, usage `/mcp` et `/api/`, révocation), sessions propres Kanbante, connexion native (avec liaison à la première visite doublement connectée), authentification Basic, pont inerte sans `DRACO_SESSION_SECRET`, compte désactivé refusé partout sans contournement JIT. La réconciliation par étiquettes Kanbante/Gitea n'est pas touchée.
|
||||
- Tests : `tests/test_draco_correspondance.py` (amorçage unique admin, deux identités → deux comptes distincts, JIT sans adoption d'un compte homonyme non lié, liaison/déliaison admin et conflit de `sub`, compte désactivé refusé, `/api/search` par utilisateur, jeton `kbt_` toujours accepté) ; le docstring de `tests/test_draco_sso.py` est mis à jour (le pont n'est plus mono-utilisateur).
|
||||
|
||||
## 0.45.12 — 2026-11-05 — Barre commune v0.4.1 : correctif des icônes invisibles du menu compte (fusion #125)
|
||||
|
||||
- **Barre d'apps v0.4.1 vendangée (dracodev-commons, branche `menu-compte-icones-svg-ns-v0-4-1`, PR commons #11)** : `static/js/draco-barre.js` remplacé **verbatim** — SHA-256 JS `82c4940e…b34f85a42` (preuve dans `tests/test_barre_v041.py`). Le CSS est inchangé (empreinte v0.4.0 conservée) ; le gabarit `_draco_bar.html` est inchangé.
|
||||
- **Correctif** : en v0.4.0, `menuIcone()` créait le `<svg>` des icônes et ses primitives via `document.createElement()` — espace de noms **HTML**, jamais dessinés par le navigateur : les libellés du menu compte étaient décalés d'un cran d'icône, sans aucun glyphe. La v0.4.1 crée tout via `document.createElementNS()` (espace de noms SVG). Glyphes, CSS et comportements inchangés.
|
||||
- **Cache** : le bump du fichier `VERSION` change le `?v=` des CSS/JS de la barre ; `APP_VERSION` suit automatiquement (source unique `VERSION`).
|
||||
- Tests : `tests/test_barre_v041.py` (verbatim v0.4.1 prouvé par SHA-256, création en espace de noms SVG) ; l'empreinte JS de `tests/test_barre_v040.py` est supersédée selon le patron habituel.
|
||||
|
||||
## 0.45.11 — 2026-11-05 — Barre commune v0.4.0 : icônes monoline du menu compte (HUB-11)
|
||||
|
||||
- **Barre d'apps v0.4.0 vendangée (dracodev-commons, branche `menu-compte-icones-v0-4-0`, PR commons #10)** : `static/css/draco-barre.css` et `static/js/draco-barre.js` remplacés **verbatim** — SHA-256 CSS `d8cf53b2…a6bd06ec`, JS `36cc24b2…e5d39a0` (preuve dans `tests/test_barre_v040.py`). Le gabarit `_draco_bar.html` est inchangé.
|
||||
- **Menu compte à icônes** : **Télécharger**, **À Propos**, **Aides**, **Profil** et **Déconnexion** reçoivent des **icônes SVG monoline** (contour, trait fin, `currentColor`, ~18 px) dans le style des icônes du rail de la console Spark — aucun emoji. L'en-tête d'identité reste sans icône ; les liens Hub (Profil → Hub compris) et le relais « Déconnexion » sont inchangés.
|
||||
- **Cache** : le bump du fichier `VERSION` change le `?v=` des CSS/JS de la barre ; `APP_VERSION` suit automatiquement (source unique `VERSION`).
|
||||
- Tests : `tests/test_barre_v040.py` (verbatim v0.4.0, marqueurs icônes) ; l'empreinte v0.3.0 de `tests/test_barre_v030.py` devient une preuve de supersession (patron HUB-1).
|
||||
|
||||
## 0.45.10 — 2026-11-04 — Barre commune v0.3.0 : liens Hub du menu compte (HUB-1)
|
||||
|
||||
> Renumérotée 0.45.10 à la publication : le numéro 0.45.9 avait été consommé entre-temps par la release du correctif « source de version » (voir l'entrée suivante), publiée avant la fusion de la PR #81.
|
||||
|
||||
- **⚠ CHANGEMENT DE COMPORTEMENT VISIBLE — « Profil » du menu compte ne mène plus à `/profile`** : Kanbante était le seul hôte à traiter l'action relayée `profil` (vers sa page `/profile` propre). La barre v0.3.0 fait de Profil un **lien vers le Hub** (`<hub>/profil`) — décision du plan Hub : un seul Profil, celui du Hub. La branche `onAction("profil")` du gabarit `_draco_bar.html` est donc **retirée**. La page `/profile` elle-même **continue d'exister** (jetons API, réglages du compte Kanbante) et reste joignable par son URL ; « Déconnexion » reste relayée vers `/logout`, inchangée. Signalé explicitement pour arbitrage à la fusion.
|
||||
- **Barre d'apps v0.3.0 vendangée (dracodev-commons, branche `hub-menu-compte-v0-3-0`, PR commons #9)** : `static/css/draco-barre.css` et `static/js/draco-barre.js` remplacés **verbatim** — SHA-256 CSS `4ac2ed2e…9475b0`, JS `9d84617b…6c74da5` (preuve dans `tests/test_barre_v030.py`). Fragment `_draco_bar.html` porté selon le mécanisme d'adaptation Kanbante existant.
|
||||
- **Menu compte** : liens **Télécharger**, **À Propos**, **Aides** et **Profil** construits par le JS vendangé depuis la base Hub résolue. Le Hub n'a pas d'onglet.
|
||||
- **URL Hub par le même mécanisme que les autres apps** (`app.config` : champ `hub_url` dans `Settings` et `UrlsDraco`) : explicite `DRACO_HUB_URL` > hôte public de la requête (motif fermé étendu à `hub<SUFFIXE>.dracodev.net`) > dérivation depuis les adresses de l'agent connues > défaut officiel `https://hub.dracodev.net` (dev : `https://hub-dev.dracodev.net`). `app/main.py` expose `draco_urls.hub` au gabarit, passé en `urls.hub`.
|
||||
- **Cache** : le bump du fichier `VERSION` change le `?v=` des CSS/JS de la barre ; `APP_VERSION` suit automatiquement depuis le correctif 0.45.9 (lecture du fichier `VERSION`, source unique).
|
||||
- Tests : `tests/test_barre_v030.py` (verbatim v0.3.0, résolution Hub, gabarit sans branche `profil`, `/profile` toujours routée) ; le test d'empreinte v0.2.0 de `tests/test_barre_v020.py` devient une preuve de supersession.
|
||||
|
||||
## 0.45.9 — 2026-11-04 — Correctif : source de version unique (fichier `VERSION`)
|
||||
|
||||
- `APP_VERSION` lit désormais le fichier `VERSION` (repli `/VERSION` dans l'image Docker, `COPY` ajouté au Dockerfile) au lieu d'une constante dans le code — la santé et l'affichage suivent toujours le fichier. Release publiée le 2026-11-04 (tag sur le commit du correctif), avant la fusion de la barre v0.3.0 (PR #81, parue en 0.45.10).
|
||||
|
||||
## 0.45.7 — 2026-11-04 — Recherche fédérée : ShaarIt, 5e source de « Rechercher partout »
|
||||
|
||||
- **`recherche.js` re-vendangé depuis dracodev-commons v0.2.1** (source canonique `ui/recherche/`) : ShaarIt devient une source de la page `/recherche` comme les autres — bases prod/dev, onglet, normaliseur (lien profond canonique `{base}shaare/<shorturl>`, contexte « Bookmark · privé · tags », repli recherche native `/?q=`). Le fichier reste **strictement identique** chez les cinq hôtes.
|
||||
- **Gabarit `/recherche`** : onglet et section « ShaarIt — bookmarks », `data-base-shaarit` rendu par `draco_urls.shaarit` (déjà résolu par `render()`), aide mise à jour.
|
||||
- **CORS de recherche** (`app/recherche.py`) : origines officielles ShaarIt prod et dev ajoutées aux listes explicites ; `shaarit` ajoutée aux clés sœurs et au dictionnaire résolu (`urls.shaarit_url`) — `GET /api/search` de Kanbante devient lisible depuis les pages de ShaarIt.
|
||||
- Tests : `tests/recherche.test.js` porté à 5 sections (83 assertions, dont le normaliseur ShaarIt) ; `tests/test_recherche.py` — allowlist attendue à 12 origines.
|
||||
- **Déploiement** : comme les versions précédentes, le conteneur Kanbante se redéploie par un geste de Bruno (la publication seule ne le redémarre pas).
|
||||
|
||||
## 0.45.6 — 2026-11-03 — Barre commune v0.2.0 : onglet ShaarIt + garde data-theme (phase 3 ShaarIt)
|
||||
|
||||
- **⚠ CHANGEMENT DE COMPORTEMENT VISIBLE — garde `data-theme` (arbitrage à la fusion)** : le CSS de barre vendangé jusqu'ici (v0.1.11) appliquait les couleurs **sombres dès que le système était en mode sombre**, sans tenir compte du thème clair choisi dans l'app — sur un poste en sombre système, la barre restait sombre alors que Kanbante était en clair. La v0.2.0 de commons corrige ce piège **dans la source** : les deux blocs `@media (prefers-color-scheme: dark)` sont gardés par `html:not([data-theme])` (le sombre système ne s'applique que si `<html>` n'a **aucun** `data-theme`). Kanbante posant toujours `html[data-theme]` (sombre par défaut, bascule mémorisée `kanbante-theme`), sa barre suivra désormais **toujours le thème de l'app**, jamais celui du système contre son choix. Aucune surcharge `style.css` ajoutée : le correctif est entièrement dans le CSS vendangé verbatim.
|
||||
- **Barre d'apps v0.2.0 vendangée (dracodev-commons @70c837a, PR commons #7)** : `static/css/draco-barre.css` et `static/js/draco-barre.js` remplacés **verbatim** — SHA-256 CSS `abc0c3b2…e05fab`, JS `24efe399…6e50e1`, identiques à la source `ui/barre-apps/` @70c837a (preuve aussi dans `tests/test_barre_v020.py`). Fragment `_draco_bar.html` porté à la v0.2.0 selon le mécanisme d'adaptation Kanbante existant (URL Jinja `draco_urls` / `agent_url`, `onAction` complet inchangé).
|
||||
- **Nouvel onglet ShaarIt** (5e app, bookmarks) : `data-app="shaarit"`, **après Kanbante, avant Console Spark** ; couleur émeraude `#059669` ; logo `static/img/logos/shaarit.svg` copié verbatim de commons (SHA-256 `faf0c8a1…316db34`). Le menu de la marque, construit depuis les onglets, l'inclut automatiquement ; le palier mobile du CSS gagne la règle `data-current="shaarit"` (sans effet chez Kanbante, hôte `kanbante`).
|
||||
- **URL ShaarIt par le même mécanisme que les autres apps** (`app.config` : nouveau champ `shaarit_url` dans `Settings` et `UrlsDraco`) : explicite `DRACO_SHAARIT_URL` > hôte public de la requête (motif fermé étendu à `shaarit<SUFFIXE>.dracodev.net`) > dérivation depuis les adresses de l'agent connues > défaut officiel `https://shaarit.dracodev.net` (dev : `https://shaarit-dev.dracodev.net`). Aucune URL codée en dur dans le gabarit ; le JS vendangé résout aussi `urls.shaarit` (défaut officiel + dérivation `-dev`). `app/main.py` expose `draco_urls.shaarit` au gabarit.
|
||||
- **Cache** : le bump 0.45.5 → 0.45.6 change aussi le `?v=` des CSS/JS de la barre.
|
||||
|
||||
## 0.45.5 — 2026-10-30 — Panneau d'alertes unifiées lisible en thème clair
|
||||
|
||||
- **Retour de Bruno (capture)** : en mode clair, le panneau « Alertes unifiées » s'affichait sombre et illisible — titre invisible, onglets vides. Cause : le CSS neutre `draco-alertes.css` (Fusion #69) attend des variables d'hôte (`--bg-primary`, `--text-dim`, `--text-tertiary`) que Kanbante ne définissait pas ; le panneau retombait sur ses replis sombres pendant que `--text` suivait le thème clair → texte sombre sur fond sombre.
|
||||
- **Correctif côté hôte** (le fichier neutre partagé n'est pas modifié) : `.draco-alertes-box` reçoit ses variables branchées sur les jetons Kanbante — `--bg-primary: var(--surface)`, `--text-dim: var(--text-2)`, `--text-tertiary: var(--text-3)` — qui suivent déjà `html[data-theme]`. Le panneau est donc correct dans les deux thèmes ; `--text` et `--border` existaient déjà.
|
||||
- **Tests** : `tests/test_alertes_theme.py` (variables branchées + fichier neutre inchangé). Vérification navigateur réel : styles calculés et captures relues en clair et en sombre.
|
||||
|
||||
## 0.45.4 — 2026-10-30 — Logo officiel en barre latérale, board clair, badge d'âge lisible
|
||||
|
||||
Retours de Bruno (captures du 2026-10-30), trois correctifs :
|
||||
|
||||
- **Logo de la barre latérale** : le pictogramme « 3 barres » est remplacé par le logo officiel de Kanbante — `static/img/logos/kanbante.png`, le même que l'onglet Kanbante de la barre d'apps commune. Le conteneur `.logo-mark` devient neutre (le PNG porte son propre fond vert) ; l'assertion du test d'identité suit.
|
||||
- **Board en thème clair** : les colonnes et le panneau « Notes du projet » gardaient le fond sombre codé en dur (`rgba(20, 24, 34, .72)`) — surcharges `html[data-theme="light"]` en blanc translucide (0.85), comme la barre latérale et l'en-tête. Le thème sombre est inchangé.
|
||||
- **Badge d'âge illisible en clair** (page En vol, Focus) : le texte jaune pâle `#fcd34d` sur fond jaune translucide passe en ambre foncé `#b45309` en thème clair ; même famille de contraste pour l'état « très vieux » et le compteur de colonne au-delà de sa limite WIP (`#dc2626`).
|
||||
- **Vérifié en navigateur réel** (styles calculés + captures relues) : colonnes blanches et badge ambre lisible en clair, aucune régression en sombre. 367 tests verts (4 nouveaux, `tests/test_logo_et_contrastes.py`).
|
||||
|
||||
## 0.45.3 — 2026-10-29 — Barre commune v0.1.11 : marque monochrome, anneau rouge de l'avatar
|
||||
|
||||
- **Barre d'apps v0.1.11 vendangée (dracodev-commons)** : `draco-barre.css` remplacé verbatim (le JS Kanbante est déjà identique à celui de commons, inchangé en v0.1.11) ; fragment `_draco_bar.html` porté à la v0.1.11, le reste de l'intégration Kanbante (URL Jinja `draco_urls` / `agent_url`, `onAction` complet y compris profil/déconnexion, compteurs) est inchangé. Les surcharges Kanbante dans `style.css` ne sont pas touchées.
|
||||
- **Marque monochrome** : la marque n'est plus le logo rouge — c'est l'étoile Spark monochrome (`static/img/brand-spark.png`, étoile blanche extraite du logo rouge, fond transparent, 26 px) : blanche sur barre foncée, noire sur barre claire via le filtre du CSS vendangé (`brightness(0)` par défaut, annulé en thème sombre). L'onglet agent et son entrée de menu gardent le logo rouge.
|
||||
- **Anneau rouge de l'avatar** : le bouton avatar flottant bas-centre gagne une bordure rouge `#c00000` de 3 px (règle du CSS vendangé, `var(--agent)`).
|
||||
- **Thème** : aucun correctif nécessaire — la barre Kanbante suit `html[data-theme]` (sombre par défaut, bascule mémorisée `kanbante-theme`) et `prefers-color-scheme`, tous deux couverts par le CSS vendangé ; vérifié, aucune surcharge `style.css` ajoutée.
|
||||
- **Cache** : le bump 0.45.2 → 0.45.3 change aussi le `?v=` des CSS/JS.
|
||||
|
||||
## 0.45.2 — 2026-10-29 — Barre commune v0.1.10 : rouge Spark, marque logo, avatar flottant, Alertes icône seule
|
||||
|
||||
- **Barre d'apps v0.1.10 vendangée (dracodev-commons)** : `draco-barre.css` remplacé verbatim (le JS Kanbante est déjà identique à celui de commons v0.1.10, inchangé) ; fragment `_draco_bar.html` porté à la v0.1.10, le reste de l'intégration Kanbante (URL Jinja `draco_urls` / `agent_url`, `onAction` complet y compris profil/déconnexion, compteurs) est inchangé. Les surcharges Kanbante dans `style.css` ne sont pas touchées.
|
||||
- **Rose → rouge Spark** : `--agent` passe de `#c2437e` à `#c00000` (rouge échantillonné dans le logo Spark) ; la marque ✦ devient le logo Spark (26 px) ; l'onglet agent passe à `--app #c00000`.
|
||||
- **Bouton Spark rose supprimé** de la barre, remplacé par le bouton avatar flottant bas-centre (`static/img/avatar-spark.png`, cercle personnage, `data-act="spark"` → le panneau Spark de l'hôte via le `onAction` existant).
|
||||
- **Alertes** : cloche seule, sans libellé — le bouton est moins large (pastille conservée).
|
||||
- **Test** : l'assertion de la marque dans `tests/test_draco_barre.py` suit le nouveau CSS (`.draco-brand-mark img` au lieu de `font-size: 24px`, disparu en v0.1.10) ; aucun test n'affirmait le bouton Spark ni le libellé Alertes.
|
||||
- **Cache** : le bump 0.45.1 → 0.45.2 change aussi le `?v=` des CSS/JS.
|
||||
|
||||
## 0.45.1 — 2026-10-29 — Barre commune : les 5 logos en place dans le menu et la barre
|
||||
|
||||
- **Barre d'apps v0.1.9 vendangée (dracodev-commons)** : `draco-barre.js` et `draco-barre.css` remplacés verbatim par la v0.1.9 (les fichiers vendangés précédents étaient identiques à l'octet près à ceux de commons avant la v0.1.9, aucune adaptation locale n'est perdue) ; fragment `_draco_bar.html` porté à la v0.1.9, le reste de l'intégration Kanbante (URL Jinja `draco_urls` / `agent_url`, `onAction` complet y compris profil/déconnexion, compteurs) est inchangé. Les surcharges Kanbante dans `style.css` ne sont pas touchées.
|
||||
- **Icônes** : les 5 tuiles de la barre — et donc du menu de la marque, qui les clone — sont désormais les logos fournis, tels quels, jamais redessinés : ObsiGate (arbre), Flowdeck (cartes), TikTak (logo officiel SVG), Kanbante (cycle vert), Console Spark (étoile rouge validée). Fichiers servis depuis `static/img/logos/` (copie de `ui/barre-apps/logos/` de commons, PNG 180 px) ; l'entrée « Console Spark » du menu clone l'icône de l'onglet agent. Les glyphes texte ▤/▦/✓/▥/✦ disparaissent des onglets (la marque ✦, le bouton Spark et le rappel d'en-tête gardent leur ✦).
|
||||
- **Cache** : le bump 0.45.0 → 0.45.1 change aussi le `?v=` des CSS/JS de la barre, donc les navigateurs ne garderont pas les anciennes icônes.
|
||||
|
||||
## 0.45.0 — 2026-10-29 — Mobile : tiroir, pied et menu compte
|
||||
|
||||
- **Tiroir mobile** : l'état « replié » mémorisé sur desktop ne rétrécit plus le tiroir (cause : `body.sidebar-collapsed .sidebar { width: 68px; }` gagnait par spécificité sur le 264 px mobile, libellés tronqués par overflow-x). Le tiroir fait 264 px, replié ou non ; le fond ne défile plus derrière lui ; le bouton de repli dans le tiroir le ferme ; un clic sur un lien de navigation le ferme aussi.
|
||||
|
||||
@@ -10,6 +10,7 @@ ENV PYTHONUNBUFFERED=1 \
|
||||
# first, copy app/ afterwards) the installed distribution was empty and
|
||||
# the app only worked by accident of the working directory.
|
||||
COPY pyproject.toml ./
|
||||
COPY VERSION ./VERSION
|
||||
COPY app/ ./app/
|
||||
COPY static/ ./static/
|
||||
RUN pip install --no-cache-dir .
|
||||
|
||||
@@ -9,7 +9,10 @@ be raised later without invalidating existing hashes.
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import re
|
||||
import secrets
|
||||
import sqlite3
|
||||
import unicodedata
|
||||
|
||||
from app import models
|
||||
|
||||
@@ -66,3 +69,130 @@ def ensure_bootstrap_admin() -> dict | None:
|
||||
display_name=settings.auth_user,
|
||||
)
|
||||
return models.get_user(user_id)
|
||||
|
||||
|
||||
# ── Correspondance draco par utilisateur (HUB-16, 0.46.0) ────
|
||||
# La correspondance identité draco ↔ compte Kanbante vit dans la
|
||||
# colonne users.draco_sub (UNIQUE, présente depuis la 0.24.0) :
|
||||
# models.get_user_by_draco(sub) est la résolution canonique.
|
||||
# resolve_draco_user() est l'unique point d'entrée du pont SSO
|
||||
# (app.main.draco_bridge) et du contrôle draco de /api/search :
|
||||
#
|
||||
# 1. correspondance existante → son compte (None si désactivé) ;
|
||||
# 2. amorçage : s'il n'existe qu'UN seul compte et que c'est un
|
||||
# admin actif sans liaison, le premier sub valide s'y lie —
|
||||
# le compte d'amorçage (celui de Bruno, créé depuis
|
||||
# l'environnement) garde ainsi son accès sans verrouillage ;
|
||||
# 3. sinon, si provisionner=True : création JIT d'un compte
|
||||
# propre à cette identité (voir provision_draco_user).
|
||||
#
|
||||
# Un sub inconnu ne désigne JAMAIS un compte existant non lié
|
||||
# (ni l'opérateur d'environnement, ni un compte dont le nom
|
||||
# ressemblerait au nom affiché draco) : la liaison d'un compte
|
||||
# préexistant est un geste d'administrateur (/admin, voir
|
||||
# docs/correspondance-draco.md) ou la première visite
|
||||
# doublement connectée (session native + cookie draco).
|
||||
|
||||
|
||||
def _slug_draco(valeur: str) -> str:
|
||||
texte = unicodedata.normalize("NFKD", valeur or "")
|
||||
texte = texte.encode("ascii", "ignore").decode("ascii").lower()
|
||||
return re.sub(r"[^a-z0-9]+", "-", texte).strip("-")[:40].strip("-")
|
||||
|
||||
|
||||
def _nom_utilisateur_draco(nom_affiche: str, sub: str) -> str:
|
||||
"""Nom d'utilisateur libre dérivé des claims (jamais réutilisé).
|
||||
|
||||
Base : le nom affiché slugifié (« Alice Exemple » →
|
||||
« alice-exemple ») ; repli « draco-<sub slugifié> ». Si le nom
|
||||
est déjà pris — y compris par un compte préexistant NON lié,
|
||||
qui ne doit pas être adopté — un suffixe -2, -3… est ajouté :
|
||||
le JIT crée toujours un compte nouveau, distinct.
|
||||
"""
|
||||
base = _slug_draco(nom_affiche)
|
||||
if len(base) < 3:
|
||||
base = _slug_draco(f"draco-{sub}") or "draco"
|
||||
candidat, n = base, 2
|
||||
while models.get_user_by_username(candidat) is not None:
|
||||
candidat = f"{base}-{n}"
|
||||
n += 1
|
||||
return candidat
|
||||
|
||||
|
||||
def provision_draco_user(sub: str, nom_affiche: str = "") -> dict | None:
|
||||
"""Création JIT du compte Kanbante d'une identité draco (HUB-16).
|
||||
|
||||
Rôle par défaut sûr : membre (« member », l'utilisateur
|
||||
standard de Kanbante) — jamais admin. Le mot de passe local
|
||||
est un secret aléatoire haché, jamais divulgué : l'accès
|
||||
normal passe par draco ; un administrateur peut poser un mot
|
||||
de passe local depuis /admin si besoin. Le compte rejoint
|
||||
l'espace Principal comme membre, comme tout nouvel
|
||||
utilisateur créé par l'administration.
|
||||
"""
|
||||
sub = str(sub or "")
|
||||
if not sub:
|
||||
return None
|
||||
existant = models.get_user_by_draco(sub)
|
||||
if existant is not None:
|
||||
return existant
|
||||
nom = (nom_affiche or "").strip()[:80]
|
||||
for _tentative in range(5):
|
||||
username = _nom_utilisateur_draco(nom, sub)
|
||||
try:
|
||||
user_id = models.create_user(
|
||||
username,
|
||||
hash_password(secrets.token_urlsafe(32)),
|
||||
role="member",
|
||||
display_name=nom,
|
||||
draco_sub=sub,
|
||||
)
|
||||
break
|
||||
except sqlite3.IntegrityError:
|
||||
# Course sur draco_sub ou username : la correspondance
|
||||
# gagnante fait foi ; sinon, réessayer un autre nom.
|
||||
existant = models.get_user_by_draco(sub)
|
||||
if existant is not None:
|
||||
return existant
|
||||
else:
|
||||
return models.get_user_by_draco(sub)
|
||||
espaces = models.list_workspaces()
|
||||
if espaces:
|
||||
models.set_member(espaces[0]["id"], user_id, "member")
|
||||
models.log_admin(user_id, "draco.jit", f"{username} (sub {sub})")
|
||||
return models.get_user(user_id)
|
||||
|
||||
|
||||
def resolve_draco_user(claims: dict | None, provisionner: bool = False) -> dict | None:
|
||||
"""Compte Kanbante désigné par des claims draco valides.
|
||||
|
||||
provisionner=False : correspondance et amorçage seulement
|
||||
(surfaces qui ne doivent pas créer de compte, p. ex.
|
||||
/api/search). provisionner=True : création JIT si aucune
|
||||
correspondance n'existe (pont /auth/draco — la « première
|
||||
connexion draco » du critère « un compte fonctionne
|
||||
partout »). Un compte désactivé résout à None et n'est
|
||||
jamais contourné par un JIT (la correspondance existe).
|
||||
"""
|
||||
sub = str((claims or {}).get("sub") or "")
|
||||
if not sub:
|
||||
return None
|
||||
lie = models.get_user_by_draco(sub)
|
||||
if lie is not None:
|
||||
return lie if lie["active"] else None
|
||||
if models.count_users() == 1:
|
||||
seul = models.list_users()[0]
|
||||
if (
|
||||
seul["role"] == "admin"
|
||||
and seul["active"]
|
||||
and not seul.get("draco_sub")
|
||||
):
|
||||
# Compte d'amorçage : liaison au premier passage.
|
||||
models.update_user(seul["id"], draco_sub=sub)
|
||||
models.log_admin(seul["id"], "draco.link.amorcage", sub)
|
||||
return models.get_user(seul["id"])
|
||||
if provisionner:
|
||||
cree = provision_draco_user(sub, str((claims or {}).get("name") or ""))
|
||||
if cree is not None and cree["active"]:
|
||||
return cree
|
||||
return None
|
||||
|
||||
@@ -130,6 +130,10 @@ Environment variables:
|
||||
DRACO_TIKTAK_URL Public base URL of TikTak for the
|
||||
common app bar. Default (official)
|
||||
https://tiktak.dracodev.net.
|
||||
DRACO_SHAARIT_URL Public base URL of ShaarIt for the
|
||||
common app bar (barre v0.2.0).
|
||||
Default (official)
|
||||
https://shaarit.dracodev.net.
|
||||
ObsiGate's bar link reuses DRACO_SSO_URL
|
||||
(the SSO emitter IS ObsiGate).
|
||||
Résolution d'une URL rendue à une
|
||||
@@ -139,7 +143,7 @@ Environment variables:
|
||||
ci-dessus (prioritaire, contrat
|
||||
inchangé) ; 2) l'hôte public de la
|
||||
requête, motif fermé
|
||||
``(og|flowdeck|tiktak|kanbante|agent)
|
||||
``(og|flowdeck|tiktak|kanbante|shaarit|agent)
|
||||
<SUFFIXE>.dracodev.net`` (suffixe vide
|
||||
en prod, ``-dev`` en dev) appliqué à
|
||||
toutes les sœurs et à l'agent — jamais
|
||||
@@ -152,7 +156,7 @@ Environment variables:
|
||||
l'URL sœur devient
|
||||
``https://<slug><SUFFIXE>.
|
||||
dracodev.net`` (slugs : og, flowdeck,
|
||||
tiktak, kanbante, agent) ; hôte hors
|
||||
tiktak, kanbante, shaarit, agent) ; hôte hors
|
||||
motif (autre domaine, LAN, IP) =
|
||||
aucune dérivation, jamais de lien
|
||||
deviné ; 4) le défaut officiel (prod).
|
||||
@@ -177,7 +181,8 @@ from urllib.parse import urlparse
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
APP_VERSION = "0.45.0"
|
||||
_version_file = next((Path(p) for p in ("/VERSION", BASE_DIR / "VERSION") if Path(p).exists()), None)
|
||||
APP_VERSION = _version_file.read_text().strip() if _version_file else "inconnue"
|
||||
|
||||
|
||||
def _env_bool(name: str, default: bool) -> bool:
|
||||
@@ -232,7 +237,7 @@ def _env_wip_limits(name: str) -> dict[str, int]:
|
||||
# du service (élargie en 0.32.2) ; 4) défaut officiel.
|
||||
_MOTIF_HOTE_AGENT = re.compile(r"agent([a-z0-9-]*)\.dracodev\.net")
|
||||
_MOTIF_HOTE_PUBLIC = re.compile(
|
||||
r"(?:og|flowdeck|tiktak|kanbante|agent)([a-z0-9-]*)\.dracodev\.net"
|
||||
r"(?:og|flowdeck|tiktak|kanbante|shaarit|agent|hub)([a-z0-9-]*)\.dracodev\.net"
|
||||
)
|
||||
|
||||
|
||||
@@ -265,7 +270,7 @@ def _suffixe_environnement_agent(url_agent: str) -> str | None:
|
||||
def suffixe_environnement_requete(hote_requete: str | None) -> str | None:
|
||||
"""Suffixe lu sur l'hôte public de la requête entrante.
|
||||
|
||||
Motif fermé ``^(og|flowdeck|tiktak|kanbante|agent)<SUFFIXE>
|
||||
Motif fermé ``^(og|flowdeck|tiktak|kanbante|shaarit|agent)<SUFFIXE>
|
||||
\\.dracodev\\.net$`` (suffixe optionnel : vide en prod,
|
||||
``-dev`` en dev) ; un port éventuel est toléré et retiré.
|
||||
Hors motif (LAN, IP, autre domaine) : ``None`` — l'appelant
|
||||
@@ -306,6 +311,8 @@ class UrlsDraco:
|
||||
sso_url: str # ObsiGate (lien de barre ; explicite = DRACO_SSO_URL)
|
||||
flowdeck_url: str
|
||||
tiktak_url: str
|
||||
shaarit_url: str # ShaarIt (barre v0.2.0 ; explicite = DRACO_SHAARIT_URL)
|
||||
hub_url: str # Hub (barre v0.3.0 ; explicite = DRACO_HUB_URL)
|
||||
public_url: str # Kanbante elle-même (KANBANTE_PUBLIC_URL)
|
||||
agent_url: str # base du service d'agents (panneau + CSP)
|
||||
|
||||
@@ -344,6 +351,10 @@ def resoudre_urls_draco(
|
||||
"DRACO_FLOWDECK_URL", "flowdeck", base.flowdeck_url
|
||||
),
|
||||
tiktak_url=resoudre("DRACO_TIKTAK_URL", "tiktak", base.tiktak_url),
|
||||
shaarit_url=resoudre(
|
||||
"DRACO_SHAARIT_URL", "shaarit", base.shaarit_url
|
||||
),
|
||||
hub_url=resoudre("DRACO_HUB_URL", "hub", base.hub_url),
|
||||
public_url=resoudre(
|
||||
"KANBANTE_PUBLIC_URL", "kanbante", base.public_url
|
||||
),
|
||||
@@ -393,6 +404,8 @@ class Settings:
|
||||
public_url: str = "https://kanbante.dracodev.net"
|
||||
flowdeck_url: str = "https://flowdeck.dracodev.net"
|
||||
tiktak_url: str = "https://tiktak.dracodev.net"
|
||||
shaarit_url: str = "https://shaarit.dracodev.net"
|
||||
hub_url: str = "https://hub.dracodev.net"
|
||||
agent_url: str = "https://agent.dracodev.net"
|
||||
archive_suggest_days: int = 180
|
||||
agent_evenements_url: str = "https://agent.dracodev.net/api/v1/evenements"
|
||||
@@ -497,6 +510,18 @@ def _env_settings() -> Settings:
|
||||
"https://tiktak.dracodev.net",
|
||||
suffixe_agent,
|
||||
),
|
||||
shaarit_url=_url_soeur_draco(
|
||||
"DRACO_SHAARIT_URL",
|
||||
"shaarit",
|
||||
"https://shaarit.dracodev.net",
|
||||
suffixe_agent,
|
||||
),
|
||||
hub_url=_url_soeur_draco(
|
||||
"DRACO_HUB_URL",
|
||||
"hub",
|
||||
"https://hub.dracodev.net",
|
||||
suffixe_agent,
|
||||
),
|
||||
agent_url=agent_url,
|
||||
archive_suggest_days=_env_int("KANBANTE_ARCHIVE_SUGGEST_DAYS", 180),
|
||||
agent_evenements_url=agent_evenements_url,
|
||||
|
||||
@@ -9,9 +9,12 @@ est présenté). Vérification hmac/hashlib — mêmes sémantiques que
|
||||
les validateurs de TikTak (0.52.0) et Flowdeck (7.74.0).
|
||||
|
||||
Le pont ``GET /auth/draco`` (app/main.py) convertit un cookie valide
|
||||
en session Kanbante native. Kanbante est mono-utilisateur
|
||||
(identifiants configurés par l'environnement) : un jeton valide
|
||||
désigne l'opérateur configuré — pas de table de liens. Sans secret
|
||||
en session Kanbante native. Depuis la 0.46.0 (HUB-16), la
|
||||
correspondance est PAR UTILISATEUR : le ``sub`` des claims désigne
|
||||
le compte Kanbante lié dans ``users.draco_sub`` (résolution et
|
||||
création JIT : app.accounts.resolve_draco_user) — un jeton valide
|
||||
ne désigne plus « l'opérateur » par hypothèse. Ce module ne fait
|
||||
que valider le jeton ; il ne connaît aucun compte. Sans secret
|
||||
configuré, module inerte.
|
||||
"""
|
||||
|
||||
|
||||
@@ -391,15 +391,24 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
||||
and _bearer_user(request) is not None:
|
||||
return await call_next(request)
|
||||
# Recherche partout (#68) : /api/search accepte aussi le jeton
|
||||
# SSO draco_session (mono-utilisateur : un jeton valide désigne
|
||||
# l'opérateur), comme les API de recherche des sœurs. Les
|
||||
# preflights OPTIONS ne portent jamais de cookies : ils sont
|
||||
# répondus par RechercheCorsMiddleware avant d'arriver ici.
|
||||
# SSO draco_session — depuis la 0.46.0 (HUB-16), par
|
||||
# utilisateur : le sub doit désigner un compte Kanbante
|
||||
# (correspondance existante ou compte d'amorçage lié au
|
||||
# passage, voir app.accounts.resolve_draco_user) ; un jeton
|
||||
# valide sans correspondance ne désigne plus l'opérateur et
|
||||
# ne suffit plus. Aucun JIT ici : la création du compte a
|
||||
# lieu à la première connexion par le pont /auth/draco.
|
||||
# Les preflights OPTIONS ne portent jamais de cookies : ils
|
||||
# sont répondus par RechercheCorsMiddleware avant d'arriver ici.
|
||||
if path == "/api/search":
|
||||
try:
|
||||
from app import accounts as accounts_service
|
||||
from app import draco
|
||||
|
||||
if draco.claims_for(request) is not None:
|
||||
claims = draco.claims_for(request)
|
||||
if claims is not None and (
|
||||
accounts_service.resolve_draco_user(claims) is not None
|
||||
):
|
||||
return await call_next(request)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -571,67 +580,50 @@ def draco_bridge(request: Request, next: str = "/"):
|
||||
"""Fusion ACC-3.2 — pont SSO : cookie ``draco_session`` → session.
|
||||
|
||||
Point de retour de ``GET /sso/authorize`` de l'émettrice
|
||||
(ObsiGate). Depuis 0.24.0 la liaison est PAR UTILISATEUR :
|
||||
si le ``sub`` draco est lié à un compte, sa session est
|
||||
créée ; si une session native est déjà ouverte, le ``sub``
|
||||
est lié à ce compte (première visite doublement connectée) ;
|
||||
sans compte lié, retour au login — la connexion native qui
|
||||
suit crée le lien (voir login_submit). Sans utilisateurs en
|
||||
base, comportement historique : session de l'opérateur.
|
||||
(ObsiGate). Depuis la 0.46.0 (HUB-16), la correspondance est
|
||||
PAR UTILISATEUR et l'hypothèse mono-utilisateur a disparu —
|
||||
résolution centralisée dans app.accounts.resolve_draco_user :
|
||||
|
||||
- session native déjà ouverte : le ``sub`` libre est lié à
|
||||
ce compte (première visite doublement connectée, inchangé) ;
|
||||
- ``sub`` lié à un compte : sa session est ouverte ;
|
||||
- compte d'amorçage (unique compte, admin, sans liaison) :
|
||||
liaison au premier passage, puis sa session — le compte
|
||||
existant de l'opérateur garde son accès ;
|
||||
- sinon : création JIT d'un compte propre à cette identité
|
||||
(rôle membre, mot de passe local aléatoire non divulgué)
|
||||
et ouverture de sa session.
|
||||
|
||||
Un ``sub`` inconnu n'ouvre plus jamais la session de
|
||||
l'opérateur d'environnement ni celle d'un compte non lié.
|
||||
Inerte sans ``DRACO_SESSION_SECRET``.
|
||||
"""
|
||||
from app import accounts as accounts_service
|
||||
from app import draco
|
||||
|
||||
settings = get_settings()
|
||||
claims = draco.claims_for(request)
|
||||
if not settings.auth_enabled or claims is None:
|
||||
return RedirectResponse("/login", status_code=303)
|
||||
sub = claims.get("sub")
|
||||
sub = str(claims.get("sub") or "")
|
||||
if not sub:
|
||||
return RedirectResponse("/login", status_code=303)
|
||||
# Déjà connecté nativement : lier le sub à ce compte.
|
||||
current = _current_user(request)
|
||||
if current is not None and current.get("id") is not None:
|
||||
if sub and not current.get("draco_sub"):
|
||||
if not current.get("draco_sub"):
|
||||
if models.get_user_by_draco(sub) is None:
|
||||
models.update_user(current["id"], draco_sub=sub)
|
||||
models.log_admin(current["id"], "draco.link", str(sub))
|
||||
models.log_admin(current["id"], "draco.link", sub)
|
||||
return RedirectResponse(_safe_next(next), status_code=303)
|
||||
# Compte lié à ce sub : ouvrir sa session.
|
||||
linked = models.get_user_by_draco(sub) if sub else None
|
||||
if linked is not None and linked["active"]:
|
||||
# Correspondance, amorçage ou création JIT : ouvrir la
|
||||
# session du compte que ce sub désigne — et seulement lui.
|
||||
user = accounts_service.resolve_draco_user(claims, provisionner=True)
|
||||
if user is not None:
|
||||
response = RedirectResponse(_safe_next(next), status_code=303)
|
||||
response.set_cookie(
|
||||
SESSION_COOKIE,
|
||||
_make_session(linked["username"]),
|
||||
max_age=SESSION_DAYS * 86400,
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
)
|
||||
return response
|
||||
if models.count_users() == 1:
|
||||
# Mono-utilisateur (cas historique) : le premier sub
|
||||
# valide se lie au compte unique et ouvre sa session —
|
||||
# c'est la sémantique livrée en 0.23.0, conservée tant
|
||||
# qu'il n'y a qu'un seul compte.
|
||||
sole = models.list_users()[0]
|
||||
if sole["active"]:
|
||||
if sub and not sole.get("draco_sub"):
|
||||
if models.get_user_by_draco(sub) is None:
|
||||
models.update_user(sole["id"], draco_sub=sub)
|
||||
response = RedirectResponse(_safe_next(next), status_code=303)
|
||||
response.set_cookie(
|
||||
SESSION_COOKIE,
|
||||
_make_session(sole["username"]),
|
||||
max_age=SESSION_DAYS * 86400,
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
)
|
||||
return response
|
||||
if models.count_users() == 0:
|
||||
# Mode env pur (aucun compte) : session de l'opérateur.
|
||||
response = RedirectResponse(_safe_next(next), status_code=303)
|
||||
response.set_cookie(
|
||||
SESSION_COOKIE,
|
||||
_make_session(settings.auth_user),
|
||||
_make_session(user["username"]),
|
||||
max_age=SESSION_DAYS * 86400,
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
@@ -1164,6 +1156,62 @@ async def admin_reset_user(
|
||||
return RedirectResponse("/admin?msg=Mot+de+passe+r%C3%A9initialis%C3%A9", status_code=303)
|
||||
|
||||
|
||||
@app.post("/admin/users/{user_id}/draco")
|
||||
async def admin_draco_user(
|
||||
request: Request,
|
||||
user_id: int,
|
||||
draco_sub: str = Form(""),
|
||||
csrf_token: str = Form(""),
|
||||
):
|
||||
"""Liaison draco par l'administrateur (HUB-16, 0.46.0).
|
||||
|
||||
C'est le mécanisme de liaison des comptes Kanbante
|
||||
préexistants : l'administrateur saisit le ``sub`` draco du
|
||||
compte (colonne Draco de /admin) ; un champ vide délie.
|
||||
Un ``sub`` déjà lié à un autre compte est refusé (la
|
||||
correspondance est biunivoque : users.draco_sub est UNIQUE).
|
||||
Contrairement au rôle et à l'activation, le geste est permis
|
||||
sur son propre compte : c'est la voie manuelle qui établit la
|
||||
correspondance du compte d'amorçage si le premier passage ne
|
||||
l'a pas fait. Le registre matriciel côté Hub (HUB-19) lira
|
||||
cette même correspondance ; ici, seul le côté Kanbante est
|
||||
requis. Procédure : docs/correspondance-draco.md.
|
||||
"""
|
||||
from urllib.parse import quote as _q
|
||||
|
||||
admin = _require_admin(request)
|
||||
if admin is None:
|
||||
return _admin_denied(request)
|
||||
if not _csrf_valid(request, csrf_token):
|
||||
return _csrf_error()
|
||||
target = models.get_user(user_id)
|
||||
if target is None:
|
||||
return RedirectResponse("/admin", status_code=303)
|
||||
sub = draco_sub.strip()
|
||||
if sub:
|
||||
autre = models.get_user_by_draco(sub)
|
||||
if autre is not None and autre["id"] != user_id:
|
||||
return RedirectResponse(
|
||||
"/admin?err="
|
||||
+ _q("Identité draco déjà liée à un autre compte"),
|
||||
status_code=303,
|
||||
)
|
||||
models.update_user(user_id, draco_sub=sub)
|
||||
models.log_admin(
|
||||
admin.get("id"), "draco.link.admin",
|
||||
f"{target['username']} : {sub}",
|
||||
)
|
||||
return RedirectResponse(
|
||||
"/admin?msg=" + _q("Liaison draco enregistrée"),
|
||||
status_code=303,
|
||||
)
|
||||
models.update_user(user_id, draco_sub=None)
|
||||
models.log_admin(admin.get("id"), "draco.unlink.admin", target["username"])
|
||||
return RedirectResponse(
|
||||
"/admin?msg=" + _q("Liaison draco retirée"), status_code=303
|
||||
)
|
||||
|
||||
|
||||
@app.get("/admin/connections")
|
||||
async def admin_connections(request: Request, msg: str = "", err: str = ""):
|
||||
admin = _require_admin(request)
|
||||
@@ -2250,6 +2298,8 @@ def render(
|
||||
"flowdeck": _urls.flowdeck_url,
|
||||
"tiktak": _urls.tiktak_url,
|
||||
"kanbante": _urls.public_url,
|
||||
"shaarit": _urls.shaarit_url,
|
||||
"hub": _urls.hub_url,
|
||||
},
|
||||
)
|
||||
context.setdefault("agent_url", _urls.agent_url)
|
||||
|
||||
@@ -1012,12 +1012,14 @@ def create_user(
|
||||
role: str = "member",
|
||||
display_name: str = "",
|
||||
must_change_password: int = 0,
|
||||
draco_sub: str | None = None,
|
||||
) -> int:
|
||||
with get_db() as conn:
|
||||
cur = conn.execute(
|
||||
"INSERT INTO users (username, password_hash, role, display_name, "
|
||||
"must_change_password) VALUES (?, ?, ?, ?, ?)",
|
||||
(username, password_hash, role, display_name, must_change_password),
|
||||
"must_change_password, draco_sub) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
(username, password_hash, role, display_name,
|
||||
must_change_password, draco_sub),
|
||||
)
|
||||
return int(cur.lastrowid)
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ PROD_ORIGINS = frozenset({
|
||||
"https://flowdeck.dracodev.net",
|
||||
"https://tiktak.dracodev.net",
|
||||
"https://kanbante.dracodev.net",
|
||||
"https://shaarit.dracodev.net",
|
||||
})
|
||||
|
||||
DEV_ORIGINS = frozenset({
|
||||
@@ -45,6 +46,7 @@ DEV_ORIGINS = frozenset({
|
||||
"https://flowdeck-dev.dracodev.net",
|
||||
"https://tiktak-dev.dracodev.net",
|
||||
"https://kanbante-dev.dracodev.net",
|
||||
"https://shaarit-dev.dracodev.net",
|
||||
})
|
||||
|
||||
AGENT_ORIGINS = frozenset({
|
||||
@@ -54,7 +56,7 @@ AGENT_ORIGINS = frozenset({
|
||||
|
||||
|
||||
# Clés du dictionnaire rendu par render() (app/main.py).
|
||||
_SOEUR_KEYS = ("obsigate", "flowdeck", "tiktak", "kanbante")
|
||||
_SOEUR_KEYS = ("obsigate", "flowdeck", "tiktak", "kanbante", "shaarit")
|
||||
|
||||
|
||||
def is_dev_host(hostname: str | None) -> bool:
|
||||
@@ -103,6 +105,7 @@ def sister_origins(
|
||||
"flowdeck": urls.flowdeck_url,
|
||||
"tiktak": urls.tiktak_url,
|
||||
"kanbante": urls.public_url,
|
||||
"shaarit": urls.shaarit_url,
|
||||
}
|
||||
except Exception:
|
||||
return PROD_ORIGINS | DEV_ORIGINS
|
||||
|
||||
@@ -1,16 +1,51 @@
|
||||
{# Barre d'apps commune dracodev — Fusion ACC-1 (issue kanbante#28).
|
||||
Snippet vendangé depuis Projets/dracodev-commons v0.1.6
|
||||
(ui/barre-apps/) : structure inchangée ; seule adaptation locale
|
||||
(0.32.0, dev/prod) = les URL des apps et l'adresse du service
|
||||
d'agents sont INJECTÉES par l'hôte depuis app.config
|
||||
(draco_urls / agent_url, défauts = noms officiels) — aucun nom
|
||||
public codé en dur dans ce gabarit ni dans draco-panneau.js.
|
||||
Snippet vendangé depuis Projets/dracodev-commons v0.3.0
|
||||
(ui/barre-apps/, branche hub-menu-compte-v0-3-0 de la PR commons
|
||||
#9 — HUB-1, fusion #115) : v0.3.0 = menu compte à liens Hub
|
||||
(Télécharger, À Propos, Aides, Profil-lien ; base
|
||||
draco_urls.hub en urls.hub ci-dessous) et branche « profil »
|
||||
de onAction RETIRÉE (voir le commentaire dans la configuration —
|
||||
la page /profile de Kanbante existe toujours, elle n'est plus
|
||||
la cible du menu compte). Structure du fragment
|
||||
commons portée ici ; seule adaptation locale (0.32.0, dev/prod)
|
||||
= les URL des apps et l'adresse du service d'agents sont
|
||||
INJECTÉES par l'hôte depuis app.config (draco_urls / agent_url,
|
||||
défauts = noms officiels) — aucun nom public codé en dur dans
|
||||
ce gabarit ni dans draco-panneau.js. Le CSS et le JS, eux, sont
|
||||
vendangés VERBATIM (static/css/draco-barre.css,
|
||||
static/js/draco-barre.js — voir CHANGELOG 0.45.10).
|
||||
v0.2.0 (phase 3 ShaarIt) : nouvel onglet ShaarIt
|
||||
(data-app="shaarit", draco_urls.shaarit), APRÈS Kanbante et
|
||||
AVANT l'onglet agent ; couleur émeraude #059669, logo
|
||||
static/img/logos/shaarit.svg (copie verbatim de commons) ;
|
||||
GARDE data-theme : le CSS v0.2.0 conditionne les blocs
|
||||
prefers-color-scheme à html:not([data-theme]) — la barre ne
|
||||
force plus le sombre système quand l'app est en thème clair
|
||||
explicite (voir CHANGELOG 0.45.6, en tête).
|
||||
Chantier 5 (fusion #69/#71, sans bump) : 5e onglet Console Spark
|
||||
(data-app="agent", agent_url), après Kanbante ; l'entrée
|
||||
« Console Spark » du menu de la marque est conservée.
|
||||
CSS/JS : /static/css/draco-barre.css, /static/js/draco-barre.js.
|
||||
Inclus par _header.html (toutes les pages applicatives ; ni le
|
||||
wallboard ni l'écran de connexion, volontaires). #}
|
||||
wallboard ni l'écran de connexion, volontaires).
|
||||
Icônes logos v0.1.9 (2026-10-29) : les tuiles sont les logos
|
||||
fournis, tels quels, servis depuis /static/img/logos/ (copie de
|
||||
ui/barre-apps/logos/ de dracodev-commons) — jamais de glyphe
|
||||
texte, jamais de redessin ; l'entrée « Console Spark » du menu
|
||||
clone l'icône de l'onglet agent (draco-barre.js).
|
||||
Portage v0.1.10 (2026-10-29) : marque = logo Spark 26 px ;
|
||||
--agent rouge Spark #c00000 ; bouton Spark rose supprimé,
|
||||
remplacé par l'avatar flottant bas-centre (data-act="spark"
|
||||
→ panneau Spark de l'hôte) ; Alertes = cloche seule.
|
||||
Portage v0.1.11 (2026-10-29) : marque = étoile Spark monochrome
|
||||
(static/img/brand-spark.png, blanche sur barre foncée, noire
|
||||
sur barre claire via le filtre du CSS vendangé) ; l'onglet
|
||||
agent et son entrée de menu gardent le logo rouge ; anneau
|
||||
rouge #c00000 autour de l'avatar flottant (CSS vendangé).
|
||||
Thème : la barre suit html[data-theme] (sombre par défaut,
|
||||
bascule mémorisée kanbante-theme) et prefers-color-scheme —
|
||||
tous deux couverts par le CSS vendangé, aucune surcharge
|
||||
style.css nécessaire. #}
|
||||
<link rel="stylesheet" href="/static/css/draco-barre.css?v={{ version }}">
|
||||
<link rel="stylesheet" href="/static/css/draco-panneau.css?v={{ version }}">
|
||||
<link rel="stylesheet" href="/static/css/draco-alertes.css?v={{ version }}">
|
||||
@@ -19,29 +54,33 @@
|
||||
<div class="draco-bar-zone draco-bar-gauche">
|
||||
<button type="button" class="draco-icon-btn" data-act="rail" aria-label="Barre latérale">☰</button>
|
||||
<a class="draco-brand" href="/" data-act="apps" aria-haspopup="true" aria-expanded="false" aria-label="dracodev — changer d'application" title="Changer d'application">
|
||||
<span class="draco-brand-mark">✦</span><span class="draco-brand-name">dracodev</span>
|
||||
<span class="draco-brand-mark"><img src="/static/img/brand-spark.png" alt="dracodev" width="26" height="26"></span><span class="draco-brand-name">dracodev</span>
|
||||
</a>
|
||||
<nav class="draco-apptabs" aria-label="Applications">
|
||||
<a class="draco-apptab" data-app="obsigate" href="{{ draco_urls.obsigate }}/">
|
||||
<span class="draco-apptab-ico" style="--app:#7a5af5">▤</span>
|
||||
<span class="draco-apptab-ico" style="--app:#7a5af5"><img src="/static/img/logos/obsigate.png" alt="ObsiGate" width="30" height="30"></span>
|
||||
<span class="draco-apptab-txt"><strong>ObsiGate</strong><small>Notes</small></span>
|
||||
</a>
|
||||
<a class="draco-apptab" data-app="flowdeck" href="{{ draco_urls.flowdeck }}/">
|
||||
<span class="draco-apptab-ico" style="--app:#2f6fed">▦</span>
|
||||
<span class="draco-apptab-ico" style="--app:#2f6fed"><img src="/static/img/logos/flowdeck.png" alt="Flowdeck" width="30" height="30"></span>
|
||||
<span class="draco-apptab-txt"><strong>Flowdeck</strong><small>Projets</small></span>
|
||||
</a>
|
||||
<a class="draco-apptab" data-app="tiktak" href="{{ draco_urls.tiktak }}/">
|
||||
<span class="draco-apptab-ico" style="--app:#e8762c">✓</span>
|
||||
<span class="draco-apptab-ico" style="--app:#e8762c"><img src="/static/img/logos/tiktak.svg" alt="TikTak" width="30" height="30"></span>
|
||||
<span class="draco-apptab-txt"><strong>TikTak</strong><small>Tâches</small></span>
|
||||
<span class="draco-badge" data-badge="tiktak" hidden>0</span>
|
||||
</a>
|
||||
<a class="draco-apptab" data-app="kanbante" href="{{ draco_urls.kanbante }}/">
|
||||
<span class="draco-apptab-ico" style="--app:#0d9488">▥</span>
|
||||
<span class="draco-apptab-ico" style="--app:#0d9488"><img src="/static/img/logos/kanbante.png" alt="Kanbante" width="30" height="30"></span>
|
||||
<span class="draco-apptab-txt"><strong>Kanbante</strong><small>Kanban</small></span>
|
||||
<span class="draco-badge" data-badge="kanbante" hidden>0</span>
|
||||
</a>
|
||||
<a class="draco-apptab" data-app="shaarit" href="{{ draco_urls.shaarit }}/">
|
||||
<span class="draco-apptab-ico" style="--app:#059669"><img src="/static/img/logos/shaarit.svg" alt="ShaarIt" width="30" height="30"></span>
|
||||
<span class="draco-apptab-txt"><strong>ShaarIt</strong><small>Bookmarks</small></span>
|
||||
</a>
|
||||
<a class="draco-apptab" data-app="agent" href="{{ agent_url }}/">
|
||||
<span class="draco-apptab-ico" style="--app:#c2437e">✦</span>
|
||||
<span class="draco-apptab-ico" style="--app:#c00000"><img src="/static/img/logos/spark.png" alt="Console Spark" width="30" height="30"></span>
|
||||
<span class="draco-apptab-txt"><strong>Console Spark</strong><small>Agent commun</small></span>
|
||||
</a>
|
||||
</nav>
|
||||
@@ -62,16 +101,12 @@
|
||||
</div>
|
||||
<button type="button" class="draco-alertes" data-act="alertes" aria-label="Alertes unifiées">
|
||||
<svg viewBox="0 0 24 24" width="19" height="19" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>
|
||||
<span class="draco-alertes-txt">Alertes</span>
|
||||
<span class="draco-badge" data-badge="alertes" hidden>0</span>
|
||||
</button>
|
||||
<button type="button" class="draco-spark" data-act="spark" aria-label="Ouvrir Spark, l'agent commun">
|
||||
<span class="draco-spark-ico">✦</span><span class="draco-spark-txt">Spark</span>
|
||||
<span class="draco-badge" data-badge="spark" hidden>0</span>
|
||||
</button>
|
||||
<button type="button" class="draco-avatar" data-act="compte" aria-label="Compte" aria-haspopup="menu" aria-expanded="false" title="Opérateur Kanbante">{{ draco_avatar }}</button>
|
||||
</div>
|
||||
</header>
|
||||
<button type="button" class="draco-spark-flottant" data-act="spark" aria-label="Ouvrir Spark, l'agent commun"><img src="/static/img/avatar-spark.png" alt="" width="64" height="64"></button>
|
||||
<div class="draco-bar-rappel" data-act="rappel-entete" hidden>✦ dracodev — afficher l'en-tête</div>
|
||||
<script>
|
||||
/* Configuration Kanbante de la barre commune (Fusion ACC-1).
|
||||
@@ -83,6 +118,12 @@
|
||||
recherche partout : page /recherche (Fusion #68). */
|
||||
window.DRACO_BAR_CONFIG = {
|
||||
current: "kanbante",
|
||||
/* Barre v0.3.0 (HUB-1) : base du Hub résolue par l'hôte
|
||||
(draco_urls.hub — DRACO_HUB_URL explicite, hôte de la
|
||||
requête, dérivation par l'agent, défaut officiel) ;
|
||||
le Hub n'a pas d'onglet, urls.hub alimente les liens
|
||||
du menu compte. */
|
||||
urls: { hub: "{{ draco_urls.hub }}/" },
|
||||
/* v0.1.6 : user = initiale auth_user ou K (draco_avatar). Sans
|
||||
cfg.user, le nouveau JS dériverait l'initiale du title
|
||||
("Opérateur Kanbante" → "O"), qui est un libellé de rôle et
|
||||
@@ -103,12 +144,15 @@
|
||||
};
|
||||
if (act === "domaine") { try { localStorage.setItem("draco_domain", el.dataset.dom); } catch (e) { } return; }
|
||||
if (act === "rail") { var b = document.getElementById("side-collapse"); if (b) b.click(); return; }
|
||||
/* Menu compte (image 2) : ces deux actions étaient relayées
|
||||
par draco-barre.js mais jamais prises en charge ici —
|
||||
le menu se fermait sans rien faire. En mobile, c'est le
|
||||
seul accès au profil et à la déconnexion (le pied de
|
||||
sidebar était masqué). */
|
||||
if (act === "profil") { location.href = "/profile"; return; }
|
||||
/* Menu compte — barre v0.3.0 (HUB-1) : « Profil » n'est
|
||||
PLUS relayé par la barre : c'est un vrai lien vers
|
||||
<hub>/profil (un seul Profil, celui du Hub — décision
|
||||
du plan Hub). L'ancienne branche onAction("profil")
|
||||
qui menait à la page /profile propre à Kanbante est
|
||||
donc RETIRÉE : la page /profile elle-même continue
|
||||
d'exister (jetons API, réglages du compte Kanbante),
|
||||
mais elle n'est plus la cible du menu compte de la
|
||||
barre. « Déconnexion » reste relayée, inchangée. */
|
||||
if (act === "deconnexion") { location.href = "/logout"; return; }
|
||||
if (act === "palette") { location.href = "/recherche"; return; }
|
||||
if (act === "alertes") { if (window.DRACO_ALERTES) { window.DRACO_ALERTES.open(); } else { location.href = "/activity"; } return; }
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
<aside class="sidebar" id="sidebar" aria-label="Navigation principale">
|
||||
<div class="side-head">
|
||||
<a href="/" class="side-logo" aria-label="Kanbante — accueil">
|
||||
<span class="logo-mark"><svg viewBox="0 0 24 24" fill="#fff" aria-hidden="true"><rect x="3" y="4" width="5" height="16" rx="1"/><rect x="9.5" y="4" width="5" height="10" rx="1"/><rect x="16" y="4" width="5" height="13" rx="1"/></svg></span>
|
||||
<span class="logo-mark"><img src="/static/img/logos/kanbante.png" alt="Kanbante" width="34" height="34"></span>
|
||||
<span class="side-label gradient-text">Kanbante</span>
|
||||
</a>
|
||||
<button type="button" class="side-search palette-trigger" title="Recherche & commandes (Ctrl+K)" aria-label="Recherche et commandes (Ctrl+K)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="M20 20l-4-4"/></svg></button>
|
||||
|
||||
@@ -20,9 +20,10 @@
|
||||
|
||||
<section class="briefing-card">
|
||||
<h2>Utilisateurs ({{ users|length }})</h2>
|
||||
<p class="notes-hint">Correspondance draco (SSO, HUB-16) : chaque identité draco (<code>sub</code>) désigne <strong>son</strong> compte Kanbante. Un compte de suite sans correspondance est créé automatiquement à sa première connexion draco (JIT, rôle membre). Pour un compte Kanbante <strong>préexistant</strong>, c'est ici que l'administrateur établit la liaison : saisir le <code>sub</code> draco du compte puis « Lier » (champ affiché = délier). Un <code>sub</code> ne peut être lié qu'à un seul compte. Procédure complète : <code>docs/correspondance-draco.md</code>.</p>
|
||||
<table class="proj-table">
|
||||
<thead>
|
||||
<tr><th>Utilisateur</th><th>Rôle</th><th>État</th><th>Draco</th><th>Dernière connexion</th><th>Actions</th></tr>
|
||||
<tr><th>Utilisateur</th><th>Rôle</th><th>État</th><th>Draco (sub)</th><th>Dernière connexion</th><th>Actions</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
@@ -38,7 +39,22 @@
|
||||
</form>
|
||||
</td>
|
||||
<td>{% if u.active %}Actif{% else %}Désactivé{% endif %}</td>
|
||||
<td>{% if u.draco_sub %}lié{% else %}—{% endif %}</td>
|
||||
<td>
|
||||
{% if u.draco_sub %}
|
||||
<code>{{ u.draco_sub }}</code>
|
||||
<form method="post" action="/admin/users/{{ u.id }}/draco" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="draco_sub" value="">
|
||||
<button type="submit" class="btn-mini">Délier</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form method="post" action="/admin/users/{{ u.id }}/draco" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="text" name="draco_sub" placeholder="sub draco" size="12" autocomplete="off">
|
||||
<button type="submit" class="btn-mini">Lier</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ u.last_login_at or '—' }}</td>
|
||||
<td class="admin-actions">
|
||||
<form method="post" action="/admin/users/{{ u.id }}/toggle" class="inline-form">
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
<button type="submit" class="btn-mini">Délier</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="muted">Pas de liaison. Elle se crée automatiquement à la première visite où ta session Kanbante et ton cookie draco coexistent (par exemple en arrivant depuis ObsiGate).</p>
|
||||
<p class="muted">Pas de liaison. Elle se crée automatiquement à la première visite où ta session Kanbante et ton cookie draco coexistent (par exemple en arrivant depuis ObsiGate) ; un administrateur peut aussi l'établir depuis la page Administration. Sans compte lié, la première connexion draco d'une identité de suite crée automatiquement son propre compte Kanbante (JIT).</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title>Rechercher partout — Kanbante</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🔍</text></svg>">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="/static/css/recherche.css">
|
||||
<link rel="stylesheet" href="/static/css/recherche.css?v={{ version }}">
|
||||
</head>
|
||||
<body>
|
||||
{% include "_header.html" %}
|
||||
@@ -16,9 +16,10 @@
|
||||
data-base-flowdeck="{{ draco_urls.flowdeck }}"
|
||||
data-base-tiktak="{{ draco_urls.tiktak }}"
|
||||
data-base-kanbante="{{ draco_urls.kanbante }}"
|
||||
data-base-shaarit="{{ draco_urls.shaarit }}"
|
||||
data-base-console="{{ agent_url }}">
|
||||
<h1>Rechercher partout</h1>
|
||||
<p class="rp-hint">Notes ObsiGate, pages Flowdeck, tâches TikTak, cartes Kanbante — dans l'environnement courant (dev reste dev). « / » ou Ctrl+K pour rechercher, ↑/↓ puis Entrée pour ouvrir un résultat. « Tout voir » bascule vers l'onglet de l'application, sans quitter la page.</p>
|
||||
<p class="rp-hint">Notes ObsiGate, pages Flowdeck, tâches TikTak, cartes Kanbante, bookmarks ShaarIt — dans l'environnement courant (dev reste dev). « / » ou Ctrl+K pour rechercher, ↑/↓ puis Entrée pour ouvrir un résultat. « Tout voir » bascule vers l'onglet de l'application, sans quitter la page.</p>
|
||||
<div class="rp-form">
|
||||
<input id="rp-q" class="rp-input" type="search" name="q" value="{{ q }}"
|
||||
placeholder="Rechercher partout…" autocomplete="off" autofocus>
|
||||
@@ -29,6 +30,7 @@
|
||||
<button type="button" class="rp-tab" role="tab" aria-selected="false" tabindex="-1" data-rp-tab="flowdeck">Flowdeck <span class="rp-tab-count"></span></button>
|
||||
<button type="button" class="rp-tab" role="tab" aria-selected="false" tabindex="-1" data-rp-tab="tiktak">TikTak <span class="rp-tab-count"></span></button>
|
||||
<button type="button" class="rp-tab" role="tab" aria-selected="false" tabindex="-1" data-rp-tab="kanbante">Kanbante <span class="rp-tab-count"></span></button>
|
||||
<button type="button" class="rp-tab" role="tab" aria-selected="false" tabindex="-1" data-rp-tab="shaarit">ShaarIt <span class="rp-tab-count"></span></button>
|
||||
<button type="button" class="rp-tab" role="tab" aria-selected="false" tabindex="-1" data-rp-tab="console">Console Spark <span class="rp-tab-count"></span></button>
|
||||
</nav>
|
||||
<section class="rp-section" data-rp-section="obsigate">
|
||||
@@ -47,6 +49,10 @@
|
||||
<div class="rp-head"><h2>Kanbante — cartes</h2><span class="rp-count"></span><a class="rp-all" href="#">Tout voir</a></div>
|
||||
<div class="rp-body"><p class="rp-state">Tapez une recherche ci-dessus.</p></div>
|
||||
</section>
|
||||
<section class="rp-section" data-rp-section="shaarit">
|
||||
<div class="rp-head"><h2>ShaarIt — bookmarks</h2><span class="rp-count"></span><a class="rp-all" href="#">Tout voir</a></div>
|
||||
<div class="rp-body"><p class="rp-state">Tapez une recherche ci-dessus.</p></div>
|
||||
</section>
|
||||
<section class="rp-section" data-rp-section="console">
|
||||
<div class="rp-head"><h2>Console Spark — discussions</h2><span class="rp-count"></span><a class="rp-all" href="#">Tout voir</a></div>
|
||||
<div class="rp-body"><p class="rp-state">Tapez une recherche ci-dessus.</p></div>
|
||||
@@ -56,6 +62,6 @@
|
||||
<footer>
|
||||
Kanbante v{{ version }} · recherche partout
|
||||
</footer>
|
||||
<script src="/static/js/recherche.js" defer></script>
|
||||
<script src="/static/js/recherche.js?v={{ version }}" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
# Correspondance draco par utilisateur (HUB-16, 0.46.0)
|
||||
|
||||
Kanbante participe au SSO de la suite dracodev : ObsiGate émet le
|
||||
cookie `draco_session` (JWS HS256, claims `sub`, `name`, `iat`,
|
||||
`exp`, `jti`), Kanbante le valide localement (`app/draco.py`) et le
|
||||
pont `GET /auth/draco` le convertit en session Kanbante native.
|
||||
|
||||
Depuis la 0.46.0, un jeton draco valide désigne **le compte
|
||||
Kanbante de cette identité** — et aucun autre. L'ancienne
|
||||
hypothèse mono-utilisateur (« un jeton valide = l'opérateur ») a
|
||||
disparu. C'est ce qui fait tenir le critère de la suite : « un
|
||||
compte créé fonctionne dans toutes les applications ».
|
||||
|
||||
## Où vit la correspondance
|
||||
|
||||
Dans le modèle existant des comptes : la colonne
|
||||
**`users.draco_sub`** (texte, `UNIQUE`, présente depuis la 0.24.0)
|
||||
porte le `sub` draco lié à chaque compte Kanbante.
|
||||
`models.get_user_by_draco(sub)` est la résolution canonique ; la
|
||||
règle de décision complète vit dans
|
||||
`app.accounts.resolve_draco_user()`, seul point d'entrée du pont
|
||||
et du contrôle draco de `/api/search`. La correspondance est donc
|
||||
biunivoque : un `sub` ↔ un compte, garanti par la contrainte
|
||||
d'unicité.
|
||||
|
||||
## Résolution d'une connexion draco (`/auth/draco`)
|
||||
|
||||
Dans l'ordre :
|
||||
|
||||
1. **Session native déjà ouverte** : si le navigateur porte à la
|
||||
fois une session Kanbante et un cookie draco dont le `sub`
|
||||
n'est lié à aucun compte, le `sub` est lié au compte de la
|
||||
session (première visite doublement connectée — mécanisme
|
||||
historique, inchangé, aussi appliqué à la connexion native
|
||||
dans `login_submit`).
|
||||
2. **Correspondance existante** : la session du compte lié est
|
||||
ouverte. Un compte **désactivé** n'ouvre aucune session et
|
||||
n'est jamais contourné (voir § Désactivation).
|
||||
3. **Compte d'amorçage** : s'il n'existe qu'**un seul compte** et
|
||||
que c'est un **admin actif sans liaison**, le premier `sub`
|
||||
valide s'y lie et sa session est ouverte. C'est le cas du
|
||||
compte créé à l'installation depuis l'environnement
|
||||
(`KANBANTE_AUTH_USER` / `KANBANTE_AUTH_PASSWORD`, Bruno) : sa
|
||||
correspondance s'établit au premier passage, sans le verrouiller
|
||||
dehors. Ses autres accès restent inchangés dans tous les cas :
|
||||
connexion native avec le mot de passe du compte, et repli
|
||||
d'urgence par les identifiants d'environnement (formulaire et
|
||||
Basic), qui ne dépendent pas de draco.
|
||||
4. **Création JIT** : sinon, un compte Kanbante est créé pour
|
||||
cette identité (détail ci-dessous) et sa session est ouverte.
|
||||
|
||||
Un `sub` inconnu n'ouvre **jamais** la session de l'opérateur
|
||||
d'environnement ni celle d'un compte existant non lié — y compris
|
||||
si le nom affiché draco ressemble au nom d'un compte local : le
|
||||
JIT crée toujours un compte **nouveau** (nom suffixé `-2`, `-3`…
|
||||
en cas de collision), jamais une adoption.
|
||||
|
||||
## Création JIT (juste-à-temps)
|
||||
|
||||
À la première connexion draco d'une identité sans correspondance :
|
||||
|
||||
- **nom d'utilisateur** : nom affiché des claims slugifié
|
||||
(`Alice Exemple` → `alice-exemple`), libre ou suffixé ;
|
||||
repli `draco-<sub>` si le nom est inexploitable ;
|
||||
- **nom d'affichage** : le `name` des claims ;
|
||||
- **rôle** : `member` (utilisateur standard) — jamais admin ;
|
||||
- **mot de passe local** : secret aléatoire, haché (PBKDF2 comme
|
||||
tous les comptes), **jamais divulgué** — l'accès normal passe
|
||||
par draco. Un administrateur peut ensuite poser un mot de
|
||||
passe local depuis `/admin` (bouton « Réinitialiser ») si un
|
||||
accès local est voulu ;
|
||||
- **espace** : le compte rejoint l'espace Principal comme membre,
|
||||
comme tout utilisateur créé par l'administration ;
|
||||
- le geste est journalisé (`draco.jit` dans le journal
|
||||
d'administration).
|
||||
|
||||
## Liaison par l'administrateur (comptes préexistants)
|
||||
|
||||
Pour associer un compte Kanbante existant à une identité draco
|
||||
sans attendre un premier passage :
|
||||
|
||||
1. Se connecter à Kanbante avec un compte **admin**.
|
||||
2. Ouvrir **Administration** (`/admin`), section **Utilisateurs**.
|
||||
3. Dans la colonne **Draco (sub)** du compte visé, saisir le
|
||||
`sub` draco de l'identité (l'identifiant du compte de suite
|
||||
chez l'émettrice, ObsiGate) et cliquer **Lier**.
|
||||
4. La liaison est immédiate et journalisée (`draco.link.admin`).
|
||||
Le bouton **Délier** de la même colonne la retire
|
||||
(`draco.unlink.admin`).
|
||||
|
||||
Gardes : un `sub` déjà lié à un autre compte est refusé avec un
|
||||
message d'erreur (délier l'autre compte d'abord). L'administrateur
|
||||
peut lier **son propre** compte — c'est la voie manuelle pour le
|
||||
compte d'amorçage si le premier passage ne l'a pas lié.
|
||||
|
||||
Chaque utilisateur peut aussi voir sa propre liaison dans
|
||||
**Mon profil → Liaison draco (SSO)** et la délier lui-même.
|
||||
|
||||
> Le registre matriciel des rôles par application côté Hub
|
||||
> (carte HUB-19) lira cette correspondance ; la présente carte ne
|
||||
> livre que le côté Kanbante, qui est la source de vérité locale.
|
||||
|
||||
## Surfaces voisines
|
||||
|
||||
- **`GET /api/search`** (« Rechercher partout ») : le cookie draco
|
||||
n'y est plus accepté « en tant qu'opérateur ». Il faut que le
|
||||
`sub` désigne un compte (correspondance existante ou compte
|
||||
d'amorçage lié au passage). Aucune création JIT sur cette
|
||||
surface : la création a lieu à la première vraie connexion, par
|
||||
le pont. Les autres modes d'accès à `/api/search` (session,
|
||||
Basic, jeton `kbt_`) sont inchangés.
|
||||
- **Désactivation** : désactiver un compte dans `/admin` coupe
|
||||
tous ses accès — session native, pont draco (la correspondance
|
||||
résout à « refusé ») et jetons `kbt_`. Réactiver le compte
|
||||
rétablit la correspondance telle quelle.
|
||||
|
||||
## Non-régressions garanties
|
||||
|
||||
- **Jetons personnels `kbt_`** : création, usage sur `/mcp` et
|
||||
`/api/`, révocation — inchangés ; un jeton n'ouvre toujours pas
|
||||
l'interface web.
|
||||
- **Sessions propres Kanbante** et **connexion native**
|
||||
(formulaire, mot de passe de la base) : inchangées, y compris
|
||||
la liaison automatique à la première visite doublement
|
||||
connectée.
|
||||
- **Authentification Basic** (identifiants d'environnement, pour
|
||||
les scripts) : inchangée.
|
||||
- **Pont inerte sans `DRACO_SESSION_SECRET`** : sans secret
|
||||
configuré, `/auth/draco` renvoie au login, comme avant.
|
||||
- La réconciliation Kanbante/Gitea par étiquettes (adoption d'une
|
||||
colonne seulement si exactement une étiquette `kanban:*` est
|
||||
reconnue) n'est pas touchée par cette carte.
|
||||
@@ -7,14 +7,39 @@
|
||||
menu des applications) ; l'avatar est un vrai <button>.
|
||||
Correctif fusion #70 (SANS bump) : bouton trois points et menu par
|
||||
défaut supprimés ; le menu de la marque gagne « Console Spark ».
|
||||
v0.1.10 : rose --agent → rouge Spark #c00000 ; marque = logo Spark ;
|
||||
bouton Spark supprimé (remplacé par l'avatar flottant bas-centre) ;
|
||||
Alertes icône seule.
|
||||
v0.1.11 : marque = étoile Spark monochrome (blanche barre foncée,
|
||||
noire barre claire) ; anneau rouge #c00000 autour de l'avatar flottant.
|
||||
v0.2.0 : nouvel onglet « ShaarIt » (data-app="shaarit", --app émeraude
|
||||
#059669, logo logos/shaarit.svg via {{LOGO_SHAARIT}}) avant l'onglet
|
||||
agent ; GARDE data-theme : les blocs prefers-color-scheme ne
|
||||
s'appliquent que si <html> n'a AUCUN attribut data-theme
|
||||
(html:not([data-theme])) — avant ce correctif, un hôte ayant choisi
|
||||
le clair via data-theme="light" héritait quand même du sombre
|
||||
système (piège constaté dans les copies vendangées de
|
||||
Kanbante/Flowdeck ; les re-vendanges de la v0.2.0 le corrigent).
|
||||
v0.3.0 (HUB-1) : aucun style nouveau — les liens du Hub du menu
|
||||
compte (Télécharger, À Propos, Aides, Profil) réutilisent
|
||||
.draco-menu-item (déjà ancre-compatible : text-decoration none)
|
||||
et le séparateur .draco-menu-sep du menu des applications.
|
||||
v0.4.0 (HUB-11) : .draco-menu-icone — les icônes monoline des
|
||||
items du menu compte (SVG inline construit par le JS) : 18 px,
|
||||
replacées à gauche du libellé par order: -1 (l'item est en
|
||||
flex), couleur atténuée --ink-2, pleine --ink au survol/focus.
|
||||
Jetons repris du prototype validé Console v7.2.
|
||||
v0.4.1 (fusion #125) : menu compte resserré — min-width
|
||||
248 → 232 px, le panneau épouse le contenu (demande de Bruno :
|
||||
le menu était trop large une fois les icônes visibles).
|
||||
Thème : clair par défaut, sombre via [data-theme="dark"] sur <html>
|
||||
ou prefers-color-scheme si l'app intégratrice ne force rien. */
|
||||
ou prefers-color-scheme si l'app intégratrice ne force rien
|
||||
(aucun attribut data-theme sur <html>). */
|
||||
|
||||
.draco-bar {
|
||||
--bg: #f2f2f0; --surface: #ffffff; --surface-2: #e9eae6; --surface-3: #f6f6f3;
|
||||
--ink: #1b1d1a; --ink-2: #5c615a; --ink-3: #787d75; --line: #dedfd9;
|
||||
--agent: #c2437e; --danger: #cf4444;
|
||||
--agent: #c00000; --danger: #cf4444;
|
||||
position: relative;
|
||||
height: 60px; box-sizing: border-box;
|
||||
display: grid; grid-template-columns: auto minmax(200px, 520px) auto;
|
||||
@@ -22,8 +47,12 @@
|
||||
background: var(--surface); border-bottom: 1px solid var(--line);
|
||||
font: 500 14px/1.3 system-ui, "Segoe UI", sans-serif; color: var(--ink);
|
||||
}
|
||||
/* Garde data-theme (v0.2.0) : le sombre système ne s'applique QUE si
|
||||
* l'hôte n'a posé aucun data-theme sur <html>. Sans cette garde, un
|
||||
* hôte en data-theme="light" sur un système sombre affichait la barre
|
||||
* sombre malgré son choix explicite. */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.draco-bar { --bg:#101211; --surface:#1a1c1a; --surface-2:#242624; --surface-3:#20221f;
|
||||
html:not([data-theme]) .draco-bar { --bg:#101211; --surface:#1a1c1a; --surface-2:#242624; --surface-3:#20221f;
|
||||
--ink:#eceee9; --ink-2:#a6aba2; --ink-3:#8b9089; --line:#2e302c; }
|
||||
}
|
||||
[data-theme="dark"] .draco-bar { --bg:#101211; --surface:#1a1c1a; --surface-2:#242624; --surface-3:#20221f;
|
||||
@@ -37,7 +66,15 @@
|
||||
.draco-brand { display: flex; align-items: center; gap: 7px; color: var(--ink); text-decoration: none; font-weight: 800; cursor: pointer; border-radius: 10px; padding: 4px 6px; margin: -4px -6px; }
|
||||
.draco-brand:hover { background: var(--surface-2); }
|
||||
.draco-brand:focus-visible { outline: 2px solid var(--agent); outline-offset: 2px; }
|
||||
.draco-brand-mark { color: var(--agent); font-size: 24px; line-height: 1; }
|
||||
.draco-brand-mark { line-height: 0; }
|
||||
/* Marque monochrome v0.1.11 : étoile blanche extraite du logo rouge,
|
||||
fond transparent — noire sur barre claire (filtre), blanche sur
|
||||
barre foncée (filtre annulé par les règles thème ci-dessous). */
|
||||
.draco-brand-mark img { width: 26px; height: 26px; object-fit: cover; display: block; filter: brightness(0); }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html:not([data-theme]) .draco-bar .draco-brand-mark img { filter: none; }
|
||||
}
|
||||
[data-theme="dark"] .draco-bar .draco-brand-mark img { filter: none; }
|
||||
|
||||
/* Menu des applications — construit par le JS depuis les onglets */
|
||||
.draco-apps-menu {
|
||||
@@ -52,7 +89,7 @@
|
||||
* droite ; ses items réutilisent .draco-apps-item. */
|
||||
.draco-compte-menu {
|
||||
position: absolute; top: calc(100% + 6px); right: 10px; z-index: 400;
|
||||
min-width: 248px; padding: 6px;
|
||||
min-width: 232px; padding: 6px;
|
||||
background: var(--surface); border: 1px solid var(--line); border-radius: 14px;
|
||||
box-shadow: 0 16px 44px rgb(0 0 0 / .28);
|
||||
}
|
||||
@@ -64,6 +101,15 @@
|
||||
text-decoration: none; cursor: pointer;
|
||||
}
|
||||
.draco-menu-item:hover, .draco-menu-item:focus-visible { background: var(--surface-2); }
|
||||
/* Icônes monoline du menu compte (v0.4.0) : appendues après le
|
||||
libellé dans le DOM, replacées à gauche par order: -1. */
|
||||
.draco-menu-icone {
|
||||
order: -1; flex: none; display: inline-flex; align-items: center;
|
||||
width: 18px; height: 18px; color: var(--ink-2);
|
||||
}
|
||||
.draco-menu-icone svg { display: block; width: 18px; height: 18px; }
|
||||
.draco-menu-item:hover .draco-menu-icone,
|
||||
.draco-menu-item:focus-visible .draco-menu-icone { color: var(--ink); }
|
||||
.draco-menu-sep { height: 1px; margin: 6px 10px; background: var(--line); }
|
||||
.draco-compte-nom {
|
||||
padding: 8px 10px 10px; font-weight: 700; font-size: 14px;
|
||||
@@ -94,6 +140,13 @@
|
||||
width: 30px; height: 30px; flex: none; display: grid; place-items: center;
|
||||
border-radius: 9px; background: var(--app); color: #fff; font-size: 15px;
|
||||
}
|
||||
/* Icônes logos (2026-10-29, corrigé le même jour) : l'image du logo
|
||||
* fourni, telle quelle, remplit la tuile 30 px — jamais de glyphe
|
||||
* texte, jamais de redessin. Le fond de la tuile est transparent :
|
||||
* les logos PNG portent déjà leur fond (dégradé), le SVG TikTak
|
||||
* reste sur le fond de la barre. */
|
||||
.draco-apptab-ico { background: transparent; }
|
||||
.draco-apptab-ico img { width: 30px; height: 30px; display: block; border-radius: 9px; object-fit: cover; }
|
||||
.draco-apptab-txt { display: none; flex-direction: column; line-height: 1.15; }
|
||||
.draco-apptab-txt strong { font-size: 14px; font-weight: 700; }
|
||||
.draco-apptab-txt small { font-size: 11.5px; color: var(--ink-2); }
|
||||
@@ -131,18 +184,23 @@
|
||||
|
||||
/* Alertes — la cloche ne disparaît JAMAIS (correctif v7.2) */
|
||||
.draco-alertes {
|
||||
position: relative; display: flex; align-items: center; gap: 7px; height: 38px;
|
||||
padding: 0 11px; border-radius: 10px; border: 1px solid var(--line);
|
||||
position: relative; display: flex; align-items: center; height: 38px;
|
||||
padding: 0 9px; border-radius: 10px; border: 1px solid var(--line);
|
||||
background: transparent; color: var(--ink); font: inherit; cursor: pointer;
|
||||
}
|
||||
|
||||
/* Spark */
|
||||
.draco-spark {
|
||||
position: relative; display: flex; align-items: center; gap: 7px; height: 38px;
|
||||
padding: 0 14px; border: 0; border-radius: 10px; cursor: pointer;
|
||||
background: var(--agent); color: #fff; font: inherit; font-weight: 700;
|
||||
/* Bouton avatar Spark flottant — bas-centre, ouvre le panneau Spark
|
||||
de l'hôte via data-act="spark" (délégation globale existante).
|
||||
La console Spark ne l'inclut pas : son fragment est propre. */
|
||||
.draco-spark-flottant {
|
||||
position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%);
|
||||
z-index: 65; width: 64px; height: 64px; padding: 0; border: 0;
|
||||
border-radius: 50%; background: transparent; cursor: pointer;
|
||||
box-shadow: 0 8px 28px rgb(0 0 0 / .45);
|
||||
}
|
||||
.draco-spark .draco-badge { border-color: var(--agent); }
|
||||
.draco-spark-flottant img { width: 64px; height: 64px; border-radius: 50%; object-fit: cover; display: block; box-sizing: border-box; border: 3px solid var(--agent); }
|
||||
.draco-spark-flottant:hover { transform: translateX(-50%) scale(1.05); }
|
||||
.draco-spark-flottant:focus-visible { outline: 2px solid var(--agent); outline-offset: 3px; }
|
||||
|
||||
.draco-icon-btn {
|
||||
width: 38px; height: 38px; border-radius: 10px; border: 1px solid transparent;
|
||||
@@ -169,7 +227,7 @@
|
||||
.draco-bar { grid-template-columns: auto minmax(200px, 440px) auto; }
|
||||
}
|
||||
@media (max-width: 1200px) {
|
||||
.draco-search-txt, .draco-search kbd, .draco-alertes-txt, .draco-spark-txt { display: none; }
|
||||
.draco-search-txt, .draco-search kbd { display: none; }
|
||||
.draco-search { width: 38px; padding: 0; justify-content: center; }
|
||||
.draco-bar { grid-template-columns: auto auto 1fr; }
|
||||
.draco-bar-droite { justify-content: flex-end; }
|
||||
@@ -193,6 +251,9 @@
|
||||
.draco-bar[data-current="flowdeck"] .draco-apptab[data-app="flowdeck"],
|
||||
.draco-bar[data-current="tiktak"] .draco-apptab[data-app="tiktak"],
|
||||
.draco-bar[data-current="kanbante"] .draco-apptab[data-app="kanbante"],
|
||||
/* v0.2.0 : sur ShaarIt elle-même (data-current="shaarit"), c'est
|
||||
l'onglet ShaarIt qui reste visible en mobile. */
|
||||
.draco-bar[data-current="shaarit"] .draco-apptab[data-app="shaarit"],
|
||||
/* 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. */
|
||||
|
||||
@@ -136,7 +136,9 @@ button, input, select, textarea { font: inherit; }
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
||||
/* Le logo officiel (kanbante.png, le même que la barre
|
||||
d'apps commune) porte son propre fond : conteneur neutre. */
|
||||
background: transparent;
|
||||
box-shadow: var(--shadow-sm);
|
||||
font-size: 1rem;
|
||||
}
|
||||
@@ -1394,7 +1396,7 @@ body.nav-closed-admin [data-group="admin"] .side-chevron { transform: rotate(-90
|
||||
}
|
||||
.side-ico { font-size: 1.05rem; width: 22px; text-align: center; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; }
|
||||
.side-ico svg { width: 20px; height: 20px; display: block; }
|
||||
.logo-mark svg { width: 20px; height: 20px; display: block; }
|
||||
.logo-mark img { width: 34px; height: 34px; display: block; border-radius: 10px; }
|
||||
.side-search svg { width: 19px; height: 19px; display: block; }
|
||||
/* Bouton thème dans la section Administration (#55) : même
|
||||
gabarit qu'un lien de navigation, mais c'est un <button>. */
|
||||
@@ -1959,6 +1961,21 @@ html[data-theme="light"] .draco-bar {
|
||||
--ink: #1b1d1a; --ink-2: #5c615a; --ink-3: #787d75; --line: #dedfd9;
|
||||
}
|
||||
|
||||
/* Alertes unifiées (Fusion #69, retour de Bruno 2026-10-30) :
|
||||
le CSS neutre draco-alertes.css attend des variables d'hôte
|
||||
(--bg-primary, --text-dim, --text-tertiary) que Kanbante ne
|
||||
définissait pas — le panneau retombait sur ses replis
|
||||
sombres alors que --text suit le thème : en mode clair,
|
||||
texte sombre sur fond sombre, panneau illisible. On branche
|
||||
les variables sur les jetons Kanbante, qui suivent déjà
|
||||
html[data-theme] — le panneau devient correct dans les
|
||||
deux thèmes. (--text et --border existent déjà.) */
|
||||
.draco-alertes-box {
|
||||
--bg-primary: var(--surface);
|
||||
--text-dim: var(--text-2);
|
||||
--text-tertiary: var(--text-3);
|
||||
}
|
||||
|
||||
/* Squelette de chargement (#65) : tuiles et colonnes, sans JS. */
|
||||
.skeleton { background: var(--surface-2); border-radius: var(--radius); animation: skeleton-pulse 1.4s ease-in-out infinite; }
|
||||
@keyframes skeleton-pulse { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }
|
||||
@@ -1980,3 +1997,17 @@ html[data-theme="light"] .draco-bar {
|
||||
sombres par défaut — ils suivent html[data-theme]. */
|
||||
html[data-theme="light"] .wall-body { background: #f2f2f0; color: #1b1d1a; }
|
||||
html[data-theme="light"] .login-body { background: #f2f2f0; }
|
||||
|
||||
/* Thème clair (retour de Bruno, 2026-10-30) : le board gardait
|
||||
ses colonnes et son panneau Notes sur le fond sombre codé en
|
||||
dur (rgba(20,24,34,.72)) — surfaces claires, comme la barre
|
||||
latérale et l'en-tête. */
|
||||
html[data-theme="light"] .kanban-column { background: rgba(255, 255, 255, 0.85); }
|
||||
html[data-theme="light"] .notes-panel { background: rgba(255, 255, 255, 0.85); }
|
||||
/* Badge d'âge des cartes (En vol, Focus) : le jaune pâle
|
||||
#fcd34d sur fond jaune translucide était illisible en clair —
|
||||
ambre et rouge foncés, contraste vérifié. Même famille :
|
||||
le compteur de colonne au-delà de sa limite WIP. */
|
||||
html[data-theme="light"] .age-badge { color: #b45309; }
|
||||
html[data-theme="light"] .age-badge.stale { color: #dc2626; }
|
||||
html[data-theme="light"] .column-count.over { color: #dc2626; }
|
||||
|
||||
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240">
|
||||
<!-- ShaarIt — signet (bookmark) blanc sur fond émeraude #059669,
|
||||
couleur signature de l'app dans la barre (#059669, distincte du
|
||||
teal Kanbante #0d9488). Fond arrondi comme les logos PNG 180 px
|
||||
des autres apps ; géométrie simple, aucun redessin d'un logo
|
||||
existant : le signet EST le symbole de l'app (bookmarks). -->
|
||||
<rect x="8" y="8" width="224" height="224" rx="48" fill="#059669"/>
|
||||
<path d="M 82 52 L 158 52 L 158 190 L 120 152 L 82 190 Z" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 569 B |
|
After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240">
|
||||
<!-- TikTak — deux arcs (les deux temps), coche-flèche. Bleu #0076fb, orange #fc750c.
|
||||
Géométrie mesurée sur le visuel validé par Bruno (variante B). -->
|
||||
<path d="M 38.2 101.1 A 84 84 0 0 1 201.8 101.1" fill="none" stroke="#0076fb" stroke-width="28" stroke-linecap="round"/>
|
||||
<path d="M 201.5 140.3 A 84 84 0 0 1 38.5 140.3" fill="none" stroke="#fc750c" stroke-width="28" stroke-linecap="round"/>
|
||||
<polyline points="80,129 106,146 163,84" fill="none" stroke="#0076fb" stroke-width="24" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 128 80 L 163 84 L 167 122" fill="none" stroke="#0076fb" stroke-width="22" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 760 B |
@@ -8,7 +8,34 @@
|
||||
* correctif fusion #70 (SANS bump de version, publication séparée) :
|
||||
* bouton trois points et son menu par défaut SUPPRIMÉS, « Console Spark »
|
||||
* ajoutée au menu de la marque, fermeture/focus corrigés (voir
|
||||
* « Menu des applications », « Focus » et « Écouteurs globaux »).
|
||||
* « Menu des applications », « Focus » et « Écouteurs globaux ») ;
|
||||
* v0.2.0 : 5e application ShaarIt — clé urls.shaarit, défaut officiel
|
||||
* https://shaarit.dracodev.net/, dérivation -dev shaarit-dev (même
|
||||
* contrat que les autres apps), entrée APP_LABELS ; l'onglet
|
||||
* (data-app="shaarit", après Kanbante, avant agent) est repris
|
||||
* automatiquement par la construction du menu de la marque.
|
||||
* v0.3.0 (carte HUB-1, fusion #115) : le Hub, point central de la
|
||||
* suite — nouvelle clé d'URL « hub » (jeton {{URL_HUB}} côté hôte,
|
||||
* DRACO_BAR_CONFIG.urls.hub), défaut officiel
|
||||
* https://hub.dracodev.net/, dérivation -dev hub-dev comme les
|
||||
* sœurs. Le menu compte gagne trois vrais liens (patron
|
||||
* buildAppsMenu : <a href>, classe draco-menu-item) — Télécharger
|
||||
* → <hub>/telecharger, À Propos → <hub>/a-propos, Aides →
|
||||
* <hub>/aide — et « Profil » devient lui aussi un lien, vers
|
||||
* <hub>/profil (il n'est plus relayé aux hôtes : un seul Profil,
|
||||
* celui du Hub). « Déconnexion » reste l'action relayée
|
||||
* (data-compte-act="deconnexion" / onAction), inchangée.
|
||||
* refreshMenuHrefs() resynchronise aussi les liens du Hub à
|
||||
* l'ouverture des menus. Le Hub n'a pas d'onglet dans la barre :
|
||||
* comme spark, sa clé est résolue et exposée via DRACO_BAR.urls,
|
||||
* sans étape « href rendu » (voir « URL » ci-dessous).
|
||||
* v0.4.0 (carte HUB-11, fusion #125) : icônes monoline dans le
|
||||
* menu compte — chaque item (les 4 liens du Hub et « Déconnexion »)
|
||||
* reçoit un glyphe SVG inline dans le style des icônes du rail de
|
||||
* la console (contour, trait fin régulier, currentColor, aucun
|
||||
* remplissage), aligné à gauche du libellé via .draco-menu-icone.
|
||||
* L'en-tête d'identité ne reçoit pas d'icône. Aucun changement de
|
||||
* comportement : liens résolus et Déconnexion relayée inchangés.
|
||||
*
|
||||
* Aucun gestionnaire inline : un seul écouteur délégué sur le document
|
||||
* (patron validé par le prototype — les onclick du HTML généré sont
|
||||
@@ -26,8 +53,10 @@
|
||||
* flowdeck: "https://flowdeck.dracodev.net/",
|
||||
* tiktak: "https://tiktak.dracodev.net/",
|
||||
* kanbante: "https://kanbante.dracodev.net/",
|
||||
* shaarit: "https://shaarit.dracodev.net/",
|
||||
* agent: "https://agent.dracodev.net/",
|
||||
* spark: "https://spark.dracodev.net/"
|
||||
* spark: "https://spark.dracodev.net/",
|
||||
* hub: "https://hub.dracodev.net/"
|
||||
* },
|
||||
* onAction: function (act, el) { … } // l'app relaie les actions
|
||||
* } // qu'elle ne connaît pas
|
||||
@@ -54,10 +83,19 @@
|
||||
* La clé spark ne correspond à aucun onglet de la barre : elle est
|
||||
* résolue et exposée via DRACO_BAR.urls pour le panneau Spark
|
||||
* (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
|
||||
* elle, a un onglet depuis le chantier 5 (ordre, v0.2.0 : ObsiGate,
|
||||
* Flowdeck, TikTak, Kanbante, ShaarIt, 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.
|
||||
* la marque et DRACO_BAR.urls. Même résolution pour shaarit (v0.2.0) :
|
||||
* explicite > dérivation -dev shaarit-dev > href rendu
|
||||
* ({{URL_SHAARIT}}) > défaut officiel.
|
||||
* Cas hub (v0.3.0) : PAS d'onglet, donc pas d'étape « href rendu » —
|
||||
* explicite (DRACO_BAR_CONFIG.urls.hub, alimenté par le jeton
|
||||
* {{URL_HUB}} remplacé côté serveur par l'hôte) > dérivation -dev
|
||||
* hub-dev sur hôte -dev > défaut officiel
|
||||
* https://hub.dracodev.net/. La base résolue sert les quatre liens
|
||||
* du menu compte (telecharger, a-propos, aide, profil).
|
||||
* Actions émises (data-act) : rail, palette, domaine (data-dom),
|
||||
* alertes, spark, rappel-entete. L'app intégratrice décide de chacune
|
||||
* (ouvrir la palette, le panneau Spark, changer de domaine…).
|
||||
@@ -132,6 +170,14 @@
|
||||
* mais le menu affiche l'identité même sans gestionnaire hôte.
|
||||
* Accessibilité : vrais <button>, aria-haspopup / aria-expanded,
|
||||
* role="menu" / "menuitem", fermeture par Échap et clic extérieur.
|
||||
* Menu compte (v0.3.0) — Profil n'est PLUS relayé : c'est un lien
|
||||
* vers <hub>/profil, comme Télécharger / À Propos / Aides (liens
|
||||
* Hub, data-hub-link, href resynchronisés à chaque ouverture via
|
||||
* refreshCompteHrefs / refreshMenuHrefs). Seule « Déconnexion »
|
||||
* reste relayée à l'hôte (data-compte-act="deconnexion"), inchangée.
|
||||
* Les hôtes qui traitaient onAction("profil") (Kanbante → /profile)
|
||||
* voient ce comportement remplacé par le lien Hub — décision du
|
||||
* plan Hub : un seul Profil, celui du Hub.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
@@ -144,8 +190,10 @@
|
||||
flowdeck: "https://flowdeck.dracodev.net/",
|
||||
tiktak: "https://tiktak.dracodev.net/",
|
||||
kanbante: "https://kanbante.dracodev.net/",
|
||||
shaarit: "https://shaarit.dracodev.net/",
|
||||
agent: "https://agent.dracodev.net/",
|
||||
spark: "https://spark.dracodev.net/"
|
||||
spark: "https://spark.dracodev.net/",
|
||||
hub: "https://hub.dracodev.net/"
|
||||
};
|
||||
|
||||
/* Dérivation -dev (v0.1.6) — appliquée UNIQUEMENT quand l'hôte courant
|
||||
@@ -157,12 +205,14 @@
|
||||
flowdeck: "https://flowdeck-dev.dracodev.net/",
|
||||
tiktak: "https://tiktak-dev.dracodev.net/",
|
||||
kanbante: "https://kanbante-dev.dracodev.net/",
|
||||
agent: "https://agent-dev.dracodev.net/"
|
||||
shaarit: "https://shaarit-dev.dracodev.net/",
|
||||
agent: "https://agent-dev.dracodev.net/",
|
||||
hub: "https://hub-dev.dracodev.net/"
|
||||
};
|
||||
|
||||
var APP_LABELS = {
|
||||
obsigate: "ObsiGate", flowdeck: "Flowdeck",
|
||||
tiktak: "TikTak", kanbante: "Kanbante"
|
||||
tiktak: "TikTak", kanbante: "Kanbante", shaarit: "ShaarIt"
|
||||
};
|
||||
|
||||
/* Vrai si hostname est un hôte -dev dracodev.net : première étiquette
|
||||
@@ -296,6 +346,28 @@
|
||||
menu.querySelectorAll(".draco-apps-item").forEach(function (a) {
|
||||
if (a.dataset.app && resolvedUrls[a.dataset.app]) a.setAttribute("href", resolvedUrls[a.dataset.app]);
|
||||
});
|
||||
/* v0.3.0 : les liens du Hub (menu compte) sont resynchronisés
|
||||
* eux aussi à l'ouverture d'un menu, pour refléter setUrls et
|
||||
* la dérivation -dev même s'ils ont été construits avant. */
|
||||
refreshCompteHrefs();
|
||||
}
|
||||
/* Liens du Hub dans le menu compte (v0.3.0) : chemin → libellé.
|
||||
* « Profil » est le quatrième lien (vers <hub>/profil) ; il
|
||||
* remplace l'ancien item relayé « profil ». */
|
||||
var HUB_LINKS = [
|
||||
["telecharger", "Télécharger"],
|
||||
["a-propos", "À Propos"],
|
||||
["aide", "Aides"],
|
||||
["profil", "Profil"]
|
||||
];
|
||||
function hubHref(path) {
|
||||
return withSlash(resolvedUrls.hub || DEFAULT_URLS.hub) + path;
|
||||
}
|
||||
function refreshCompteHrefs() {
|
||||
if (!compteMenu) return;
|
||||
compteMenu.querySelectorAll("[data-hub-link]").forEach(function (a) {
|
||||
a.setAttribute("href", hubHref(a.getAttribute("data-hub-link")));
|
||||
});
|
||||
}
|
||||
function togglePopup(menu, btn, force) {
|
||||
if (!menu) return;
|
||||
@@ -365,11 +437,15 @@
|
||||
if (badge) a.appendChild(badge.cloneNode(true));
|
||||
appsMenu.appendChild(a);
|
||||
});
|
||||
/* Entrée « Console Spark » (#70) : après Kanbante, vers la console
|
||||
/* Entrée « Console Spark » (#70) : après les onglets d'apps
|
||||
* (ObsiGate…ShaarIt, v0.2.0), vers la console
|
||||
* de l'agent commun (URL agent résolue — explicite > -dev >
|
||||
* défaut prod ; aucun hôte spark-dev inventé). data-app="agent"
|
||||
* l'intègre à refreshMenuHrefs/applyUrls/setCurrent comme les
|
||||
* autres items ; l'icône ✦ reprend la couleur agent du CSS. */
|
||||
* autres items ; son icône CLONE celle de l'onglet agent
|
||||
* (2026-10-29, logos barre-apps, corrigé le même jour) : le logo
|
||||
* fourni, tel quel, jamais redessiné — le CSS --agent (rose) des
|
||||
* autres éléments de la barre n'est pas changé ici. */
|
||||
var sep = document.createElement("div");
|
||||
sep.className = "draco-menu-sep";
|
||||
sep.setAttribute("aria-hidden", "true");
|
||||
@@ -379,10 +455,18 @@
|
||||
sparkItem.href = resolvedUrls.agent || "#";
|
||||
sparkItem.dataset.app = "agent";
|
||||
sparkItem.setAttribute("role", "menuitem");
|
||||
var sparkIco = document.createElement("span");
|
||||
sparkIco.className = "draco-apptab-ico";
|
||||
sparkIco.setAttribute("style", "--app:#c2437e");
|
||||
sparkIco.textContent = "✦";
|
||||
var sparkIco;
|
||||
var agentTab = document.querySelector('.draco-apptab[data-app="agent"]');
|
||||
var agentIco = agentTab ? agentTab.querySelector(".draco-apptab-ico") : null;
|
||||
if (agentIco) {
|
||||
sparkIco = agentIco.cloneNode(true);
|
||||
} else {
|
||||
sparkIco = document.createElement("span");
|
||||
sparkIco.className = "draco-apptab-ico";
|
||||
sparkIco.setAttribute("style", "--app:#d21f2c");
|
||||
sparkIco.setAttribute("data-icon", "agent");
|
||||
sparkIco.textContent = "✦";
|
||||
}
|
||||
sparkItem.appendChild(sparkIco);
|
||||
var sparkTxt = document.createElement("span");
|
||||
sparkTxt.className = "draco-apps-item-txt";
|
||||
@@ -406,10 +490,80 @@
|
||||
if (!appsMenu.hidden) lastOpener = brand;
|
||||
}
|
||||
|
||||
/* Menu compte (v0.1.6) — en-tête d'identité + Profil / Déconnexion
|
||||
* relayés à l'hôte. L'en-tête est rafraîchi à chaque ouverture, pour
|
||||
* refléter un title posé après coup par l'hôte (ex. ObsiGate, qui
|
||||
* remplit l'avatar depuis /api/auth/me après chargement). */
|
||||
/* Icônes monoline du menu compte (v0.4.0) — glyphes contour
|
||||
* dans le style des icônes du rail de la console : viewBox 24,
|
||||
* stroke currentColor, remplissage nul, extrémités rondes.
|
||||
* Chaque glyphe est une liste de primitives [balise, attributs]
|
||||
* construites en DOM (jamais d'innerHTML), comme le reste du
|
||||
* fichier. Le libellé reste le nœud texte de l'item ; l'icône,
|
||||
* appendue après, est replacée à gauche par le CSS (order: -1
|
||||
* sur .draco-menu-icone, l'item étant déjà en flex). */
|
||||
var MENU_ICONES = {
|
||||
/* Flèche vers le bas dans un plateau. */
|
||||
"telecharger": [
|
||||
["path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }],
|
||||
["polyline", { points: "7 10 12 15 17 10" }],
|
||||
["line", { x1: "12", y1: "15", x2: "12", y2: "3" }]
|
||||
],
|
||||
/* Cercle d'information. */
|
||||
"a-propos": [
|
||||
["circle", { cx: "12", cy: "12", r: "9" }],
|
||||
["line", { x1: "12", y1: "11", x2: "12", y2: "16" }],
|
||||
["line", { x1: "12", y1: "8", x2: "12.01", y2: "8" }]
|
||||
],
|
||||
/* Cercle d'interrogation. */
|
||||
"aide": [
|
||||
["circle", { cx: "12", cy: "12", r: "9" }],
|
||||
["path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }],
|
||||
["line", { x1: "12", y1: "17", x2: "12.01", y2: "17" }]
|
||||
],
|
||||
/* Personne : tête + épaules. */
|
||||
"profil": [
|
||||
["path", { d: "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" }],
|
||||
["circle", { cx: "12", cy: "7", r: "4" }]
|
||||
],
|
||||
/* Flèche sortant d'un cadre (porte). */
|
||||
"deconnexion": [
|
||||
["path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }],
|
||||
["polyline", { points: "16 17 21 12 16 7" }],
|
||||
["line", { x1: "21", y1: "12", x2: "9", y2: "12" }]
|
||||
]
|
||||
};
|
||||
/* Espace de noms SVG — OBLIGATOIRE : un élément « svg » (ou
|
||||
* « path », « circle »…) créé par createElement() appartient à
|
||||
* l'espace de noms HTML et le navigateur ne le dessine JAMAIS
|
||||
* (bug v0.4.0 : libellés décalés d'un cran d'icône, aucun glyphe
|
||||
* visible ; corrigé en v0.4.1, carte fusion #125). */
|
||||
var SVG_NS = "http://www.w3.org/2000/svg";
|
||||
function menuIcone(nom) {
|
||||
var span = document.createElement("span");
|
||||
span.className = "draco-menu-icone";
|
||||
span.setAttribute("aria-hidden", "true");
|
||||
var svg = document.createElementNS(SVG_NS, "svg");
|
||||
svg.setAttribute("viewBox", "0 0 24 24");
|
||||
svg.setAttribute("fill", "none");
|
||||
svg.setAttribute("stroke", "currentColor");
|
||||
svg.setAttribute("stroke-width", "1.8");
|
||||
svg.setAttribute("stroke-linecap", "round");
|
||||
svg.setAttribute("stroke-linejoin", "round");
|
||||
svg.setAttribute("focusable", "false");
|
||||
(MENU_ICONES[nom] || []).forEach(function (spec) {
|
||||
var el = document.createElementNS(SVG_NS, spec[0]);
|
||||
for (var k in spec[1]) el.setAttribute(k, spec[1][k]);
|
||||
svg.appendChild(el);
|
||||
});
|
||||
span.appendChild(svg);
|
||||
return span;
|
||||
}
|
||||
|
||||
/* Menu compte (v0.1.6 ; v0.3.0 pour les liens) — en-tête
|
||||
* d'identité, puis les liens du Hub (Télécharger, À Propos,
|
||||
* Aides, Profil — vrais <a href>, même patron que le menu des
|
||||
* applications), un séparateur, puis « Déconnexion » relayée à
|
||||
* l'hôte. L'en-tête et les href sont rafraîchis à chaque
|
||||
* ouverture, pour refléter un title posé après coup par l'hôte
|
||||
* (ex. ObsiGate, qui remplit l'avatar depuis /api/auth/me après
|
||||
* chargement) et la résolution d'URL la plus récente. */
|
||||
function buildCompteMenu() {
|
||||
if (!bar || !avatarBtn || compteMenu) return;
|
||||
compteMenu = document.createElement("div");
|
||||
@@ -420,15 +574,28 @@
|
||||
var head = document.createElement("div");
|
||||
head.className = "draco-compte-nom";
|
||||
compteMenu.appendChild(head);
|
||||
[["profil", "Profil"], ["deconnexion", "Déconnexion"]].forEach(function (pair) {
|
||||
var b = document.createElement("button");
|
||||
b.type = "button";
|
||||
b.className = "draco-menu-item";
|
||||
b.setAttribute("data-compte-act", pair[0]);
|
||||
b.setAttribute("role", "menuitem");
|
||||
b.textContent = pair[1];
|
||||
compteMenu.appendChild(b);
|
||||
HUB_LINKS.forEach(function (pair) {
|
||||
var a = document.createElement("a");
|
||||
a.className = "draco-menu-item";
|
||||
a.href = hubHref(pair[0]);
|
||||
a.setAttribute("data-hub-link", pair[0]);
|
||||
a.setAttribute("role", "menuitem");
|
||||
a.textContent = pair[1];
|
||||
a.appendChild(menuIcone(pair[0]));
|
||||
compteMenu.appendChild(a);
|
||||
});
|
||||
var sep = document.createElement("div");
|
||||
sep.className = "draco-menu-sep";
|
||||
sep.setAttribute("aria-hidden", "true");
|
||||
compteMenu.appendChild(sep);
|
||||
var b = document.createElement("button");
|
||||
b.type = "button";
|
||||
b.className = "draco-menu-item";
|
||||
b.setAttribute("data-compte-act", "deconnexion");
|
||||
b.setAttribute("role", "menuitem");
|
||||
b.textContent = "Déconnexion";
|
||||
b.appendChild(menuIcone("deconnexion"));
|
||||
compteMenu.appendChild(b);
|
||||
bar.appendChild(compteMenu);
|
||||
}
|
||||
function refreshCompteMenu() {
|
||||
@@ -436,6 +603,7 @@
|
||||
var head = compteMenu.querySelector(".draco-compte-nom");
|
||||
var name = displayName();
|
||||
if (head) head.textContent = name || "Compte";
|
||||
refreshCompteHrefs();
|
||||
}
|
||||
function toggleCompteMenu(force) {
|
||||
buildCompteMenu();
|
||||
@@ -532,6 +700,7 @@
|
||||
resolvedUrls = resolveUrls();
|
||||
window.DRACO_BAR.urls = resolvedUrls;
|
||||
applyUrls(resolvedUrls);
|
||||
refreshCompteHrefs();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
* chantier 2+3 : onglets, « Tout voir », focus).
|
||||
*
|
||||
* Vanilla, sans dépendance. Un seul fichier vendangé tel quel par chaque
|
||||
* hôte (ObsiGate, Flowdeck, TikTak, Kanbante) : la configuration vient
|
||||
* des attributs data-* de #recherche, rendus côté serveur par l'hôte.
|
||||
* hôte (ObsiGate, Flowdeck, TikTak, Kanbante, ShaarIt) : la configuration
|
||||
* vient des attributs data-* de #recherche, rendus côté serveur par
|
||||
* l'hôte. Source canonique : dracodev-commons, ui/recherche/
|
||||
* (contrat : contrats/recherche-federee.md).
|
||||
*
|
||||
* Présentation en onglets : Tout, ObsiGate, Flowdeck, TikTak, Kanbante,
|
||||
* Console Spark. « Tout » = les 4 sections groupées, chaque liste
|
||||
* ShaarIt, Console Spark. « Tout » = les 5 sections groupées, chaque liste
|
||||
* tronquée à PER_APP_LIMIT ; un onglet application = tous les résultats
|
||||
* de cette application (aucune troncature côté client — la requête
|
||||
* ObsiGate part avec limit=FULL_LIMIT, les autres API renvoient leur
|
||||
@@ -46,6 +48,11 @@
|
||||
* -> { groups: [{ full_name, rows: [{ issue, column }] }] }
|
||||
* Lien profond : {base}board/<full_name> (la carte n'a pas d'URL propre
|
||||
* adressable — même limite que la page /search native de Kanbante).
|
||||
* - ShaarIt : GET {base}/api/search?q=
|
||||
* -> { links: [{ id, title, url, description, tags, shorturl,
|
||||
* private }] }
|
||||
* Lien profond (contrat liens-profonds) : {base}shaare/<shorturl>.
|
||||
* Recherche native adressable : {base}?q=<q> (page d'accueil).
|
||||
* - Console Spark : GET {base-console}/api/v1/recherche?q=&limite=
|
||||
* (agent-commun, authentifié par le cookie SSO draco_session) ->
|
||||
* { q, domain, limite_par_type, resultats[], par_type,
|
||||
@@ -87,8 +94,9 @@
|
||||
* Authentification cross-origin : les appels sœurs partent avec
|
||||
* credentials:"include". Le navigateur envoie alors les cookies de
|
||||
* chaque origine, dont le cookie SSO partagé draco_session
|
||||
* (Domain=.dracodev.net), accepté par les 4 API de recherche
|
||||
* (ObsiGate et TikTak nativement, Flowdeck et Kanbante via #68)
|
||||
* (Domain=.dracodev.net), accepté par les 5 API de recherche
|
||||
* (ObsiGate et TikTak nativement, Flowdeck et Kanbante via #68,
|
||||
* ShaarIt via la fédération v0.2.1 de commons)
|
||||
* et par GET /api/v1/recherche de la console (identity() accepte
|
||||
* draco_session). Chaque sœur n'autorise en CORS que les origines
|
||||
* sœurs (GET sur l'endpoint de recherche uniquement, pas de "*") ;
|
||||
@@ -109,12 +117,14 @@
|
||||
flowdeck: "https://flowdeck.dracodev.net/",
|
||||
tiktak: "https://tiktak.dracodev.net/",
|
||||
kanbante: "https://kanbante.dracodev.net/",
|
||||
shaarit: "https://shaarit.dracodev.net/",
|
||||
};
|
||||
var DEV = {
|
||||
obsigate: "https://og-dev.dracodev.net/",
|
||||
flowdeck: "https://flowdeck-dev.dracodev.net/",
|
||||
tiktak: "https://tiktak-dev.dracodev.net/",
|
||||
kanbante: "https://kanbante-dev.dracodev.net/",
|
||||
shaarit: "https://shaarit-dev.dracodev.net/",
|
||||
};
|
||||
var PROD_AGENT = "https://agent.dracodev.net/";
|
||||
var DEV_AGENT = "https://agent-dev.dracodev.net/";
|
||||
@@ -125,6 +135,7 @@
|
||||
{ key: "flowdeck", label: "Flowdeck" },
|
||||
{ key: "tiktak", label: "TikTak" },
|
||||
{ key: "kanbante", label: "Kanbante" },
|
||||
{ key: "shaarit", label: "ShaarIt" },
|
||||
];
|
||||
var CONSOLE_APP = { key: "console", label: "Console Spark" };
|
||||
var CONSOLE_TYPE_LABELS = {
|
||||
@@ -145,7 +156,7 @@
|
||||
planification: "/?section=planifie",
|
||||
identite: "/?section=identites",
|
||||
};
|
||||
var TABS = ["all", "obsigate", "flowdeck", "tiktak", "kanbante", "console"];
|
||||
var TABS = ["all", "obsigate", "flowdeck", "tiktak", "kanbante", "shaarit", "console"];
|
||||
var COLUMN_LABELS = {
|
||||
backlog: "Backlog",
|
||||
in_progress: "En cours",
|
||||
@@ -227,7 +238,7 @@
|
||||
return TABS.indexOf(v) !== -1 ? v : "all";
|
||||
}
|
||||
|
||||
/* Sections visibles pour un onglet. « Tout » = les 4 sections
|
||||
/* Sections visibles pour un onglet. « Tout » = les 5 sections
|
||||
* groupées (vue #68) ; la console n'y figure pas (voir en-tête). */
|
||||
function visibleKeys(tab) {
|
||||
tab = parseTab(tab);
|
||||
@@ -256,7 +267,7 @@
|
||||
* Chargement -> "…", ok/vide -> total ("0" honnête),
|
||||
* erreur/indisponible/inactif -> "" (pas de faux 0).
|
||||
* Tout : "…" tant qu'une section charge, sinon somme des totaux
|
||||
* connus, "" si les 4 sections sont en erreur/inactives. */
|
||||
* connus, "" si les 5 sections sont en erreur/inactives. */
|
||||
function tabCounts(states) {
|
||||
states = states || {};
|
||||
var out = {};
|
||||
@@ -367,6 +378,22 @@
|
||||
};
|
||||
});
|
||||
}
|
||||
if (app === "shaarit") {
|
||||
return cut(data.links || []).map(function (l) {
|
||||
var tags = (l.tags || []).filter(function (t) { return !!t; });
|
||||
var ctx = "Bookmark";
|
||||
if (l.private) ctx += " · privé";
|
||||
if (tags.length) ctx += " · " + tags.join(", ");
|
||||
return {
|
||||
title: l.title || l.url || "Sans titre",
|
||||
snippet: snippetOf(l.description || ""),
|
||||
context: ctx,
|
||||
/* Lien profond canonique (contrats/liens-profonds.md) ;
|
||||
* sans shorturl, repli sur l'URL du bookmark lui-même. */
|
||||
url: l.shorturl ? base + "shaare/" + l.shorturl : l.url || null,
|
||||
};
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -455,13 +482,16 @@
|
||||
if (app === "tiktak" || app === "kanbante") {
|
||||
return base + "search?q=" + encodeURIComponent(q || "");
|
||||
}
|
||||
if (app === "shaarit") {
|
||||
return base + "?q=" + encodeURIComponent(q || "");
|
||||
}
|
||||
/* ObsiGate et Flowdeck n'ont pas de page de recherche adressable :
|
||||
* leur recherche native vit dans la palette (Ctrl+K) de l'app. */
|
||||
return base;
|
||||
}
|
||||
|
||||
function nativeSearchLabel(app) {
|
||||
if (app === "tiktak" || app === "kanbante") {
|
||||
if (app === "tiktak" || app === "kanbante" || app === "shaarit") {
|
||||
return "Ouvrir la recherche dans " + appLabel(app);
|
||||
}
|
||||
return "Ouvrir " + appLabel(app) + " (recherche via la palette Ctrl+K)";
|
||||
|
||||
@@ -61,8 +61,18 @@ items = rp.normalize("kanbante", { groups: [{ full_name: "Projets/site", rows: [
|
||||
ok(items[0].title === "#12 Bug" && items[0].url === "https://kanbante-dev.dracodev.net/board/Projets/site", "lien tableau Kanbante");
|
||||
ok(items[0].context === "Projets/site · En cours", "contexte Kanbante");
|
||||
|
||||
/* normalize ShaarIt (commons v0.2.1) : lien profond shaare/<shorturl>,
|
||||
* contexte privé + tags, repli sur l'URL du bookmark sans shorturl */
|
||||
items = rp.normalize("shaarit", { links: [{ title: "Yandex", url: "https://yandex.com", description: "moteur", tags: ["recherche", "web"], shorturl: "xYz9", private: true }] }, "https://shaarit-dev.dracodev.net/", "recherche");
|
||||
ok(items[0].title === "Yandex" && items[0].url === "https://shaarit-dev.dracodev.net/shaare/xYz9", "lien profond ShaarIt (shaare/<shorturl>)");
|
||||
ok(items[0].context === "Bookmark · privé · recherche, web", "contexte ShaarIt (privé + tags)");
|
||||
ok(items[0].snippet === "moteur", "extrait ShaarIt = description");
|
||||
items = rp.normalize("shaarit", { links: [{ title: "", url: "https://exemple.fr", tags: [], shorturl: "" }] }, "https://shaarit.dracodev.net/", "x");
|
||||
ok(items[0].url === "https://exemple.fr" && items[0].title === "https://exemple.fr", "ShaarIt sans shorturl : repli URL + titre");
|
||||
|
||||
/* nativeSearchUrl */
|
||||
ok(rp.nativeSearchUrl("tiktak", "https://tiktak-dev.dracodev.net/", "q") === "https://tiktak-dev.dracodev.net/search?q=q", "repli TikTak");
|
||||
ok(rp.nativeSearchUrl("shaarit", "https://shaarit-dev.dracodev.net/", "q") === "https://shaarit-dev.dracodev.net/?q=q", "repli ShaarIt = accueil ?q=");
|
||||
ok(rp.nativeSearchUrl("obsigate", "https://og-dev.dracodev.net/", "q") === "https://og-dev.dracodev.net/", "repli ObsiGate = accueil (palette)");
|
||||
|
||||
|
||||
@@ -70,15 +80,17 @@ ok(rp.nativeSearchUrl("obsigate", "https://og-dev.dracodev.net/", "q") === "http
|
||||
|
||||
/* parseTab */
|
||||
ok(rp.parseTab("obsigate") === "obsigate", "onglet valide conservé");
|
||||
ok(rp.parseTab("shaarit") === "shaarit", "onglet shaarit valide");
|
||||
ok(rp.parseTab("console") === "console", "onglet console valide");
|
||||
ok(rp.parseTab("bidon") === "all", "onglet inconnu -> Tout");
|
||||
ok(rp.parseTab(undefined) === "all", "onglet absent -> Tout");
|
||||
|
||||
/* visibleKeys : Tout = les 4 sections groupées, sans la console */
|
||||
ok(JSON.stringify(rp.visibleKeys("all")) === JSON.stringify(["obsigate", "flowdeck", "tiktak", "kanbante"]), "Tout = 4 sections, pas la console");
|
||||
/* visibleKeys : Tout = les 5 sections groupées, sans la console */
|
||||
ok(JSON.stringify(rp.visibleKeys("all")) === JSON.stringify(["obsigate", "flowdeck", "tiktak", "kanbante", "shaarit"]), "Tout = 5 sections, pas la console");
|
||||
ok(JSON.stringify(rp.visibleKeys("tiktak")) === JSON.stringify(["tiktak"]), "onglet app = sa seule section");
|
||||
ok(JSON.stringify(rp.visibleKeys("shaarit")) === JSON.stringify(["shaarit"]), "onglet shaarit = sa seule section");
|
||||
ok(JSON.stringify(rp.visibleKeys("console")) === JSON.stringify(["console"]), "onglet console = sa seule section");
|
||||
ok(JSON.stringify(rp.visibleKeys("bidon")) === JSON.stringify(["obsigate", "flowdeck", "tiktak", "kanbante"]), "onglet inconnu -> Tout");
|
||||
ok(JSON.stringify(rp.visibleKeys("bidon")) === JSON.stringify(["obsigate", "flowdeck", "tiktak", "kanbante", "shaarit"]), "onglet inconnu -> Tout");
|
||||
|
||||
/* pageItems : troncature Tout (8), complet dans l'onglet de l'app */
|
||||
let douze = Array.from({ length: 12 }, (_, i) => ({ title: "T" + i }));
|
||||
@@ -91,9 +103,11 @@ let c = rp.tabCounts({
|
||||
flowdeck: { status: "ok", total: 3 },
|
||||
tiktak: { status: "empty", total: 0 },
|
||||
kanbante: { status: "error", total: 0 },
|
||||
shaarit: { status: "ok", total: 2 },
|
||||
console: { status: "idle", total: 0 },
|
||||
});
|
||||
ok(c.obsigate === "…", "compteur … pendant le chargement");
|
||||
ok(c.shaarit === "2", "compteur shaarit");
|
||||
ok(c.flowdeck === "3" && c.tiktak === "0", "compteur total, vide honnête à 0");
|
||||
ok(c.kanbante === "" && c.console === "", "erreur/inactif : pas de faux 0");
|
||||
ok(c.all === "…", "Tout = … tant qu'une section charge");
|
||||
@@ -102,18 +116,20 @@ c = rp.tabCounts({
|
||||
flowdeck: { status: "ok", total: 2 },
|
||||
tiktak: { status: "empty", total: 0 },
|
||||
kanbante: { status: "error", total: 0 },
|
||||
shaarit: { status: "ok", total: 1 },
|
||||
console: { status: "ok", total: 4 },
|
||||
});
|
||||
ok(c.all === "7", "Tout = somme des totaux connus");
|
||||
ok(c.all === "8", "Tout = somme des totaux connus (5 sections)");
|
||||
ok(c.console === "4", "compteur console");
|
||||
c = rp.tabCounts({
|
||||
obsigate: { status: "error", total: 0 },
|
||||
flowdeck: { status: "error", total: 0 },
|
||||
tiktak: { status: "error", total: 0 },
|
||||
kanbante: { status: "error", total: 0 },
|
||||
shaarit: { status: "error", total: 0 },
|
||||
console: { status: "idle", total: 0 },
|
||||
});
|
||||
ok(c.all === "", "Tout vide si les 4 sections sont en erreur");
|
||||
ok(c.all === "", "Tout vide si les 5 sections sont en erreur");
|
||||
|
||||
/* buildSearchUrl : état onglet partageable */
|
||||
ok(rp.buildSearchUrl("café", "tiktak") === "?q=caf%C3%A9&tab=tiktak", "URL q+onglet");
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Panneau d'alertes unifiées (Fusion #69) et thèmes.
|
||||
|
||||
Le CSS neutre draco-alertes.css attend des variables d'hôte
|
||||
(--bg-primary, --text-dim, --text-tertiary). Kanbante ne les
|
||||
définissait pas : le panneau retombait sur ses replis sombres,
|
||||
illisible en thème clair (retour de Bruno, 2026-10-30).
|
||||
L'hôte les branche sur ses propres jetons, qui suivent
|
||||
html[data-theme].
|
||||
"""
|
||||
|
||||
|
||||
def test_alertes_variables_hote_branchees(client, fake_gitea):
|
||||
css = client.get("/static/css/style.css").text
|
||||
assert ".draco-alertes-box {" in css
|
||||
assert "--bg-primary: var(--surface);" in css
|
||||
assert "--text-dim: var(--text-2);" in css
|
||||
assert "--text-tertiary: var(--text-3);" in css
|
||||
|
||||
|
||||
def test_alertes_css_neutre_inchange(client, fake_gitea):
|
||||
# Le fichier partagé n'est pas modifié côté Kanbante :
|
||||
# il continue de consommer les variables d'hôte.
|
||||
css = client.get("/static/css/draco-alertes.css").text
|
||||
assert "background: var(--bg-primary, #1f1f1f);" in css
|
||||
assert "color: var(--text-dim, #999);" in css
|
||||
@@ -0,0 +1,113 @@
|
||||
"""Barre commune v0.2.0 (phase 3 ShaarIt) — re-vendange 0.45.6.
|
||||
|
||||
- CSS/JS vendangés VERBATIM depuis dracodev-commons @70c837a
|
||||
(ui/barre-apps/barre-apps.css / .js) ; logo shaarit.svg idem.
|
||||
- GARDE data-theme (changement de comportement visible) : avant
|
||||
la v0.2.0, le CSS vendangé appliquait le sombre système dès
|
||||
``prefers-color-scheme: dark``, même avec un thème clair
|
||||
explicite (html[data-theme="light"]) ; la v0.2.0 conditionne
|
||||
les deux blocs à ``html:not([data-theme])``.
|
||||
- Onglet ShaarIt : présent dans le rendu, APRÈS Kanbante et
|
||||
AVANT l'agent, URL résolue par le même mécanisme que les
|
||||
autres apps (explicite DRACO_SHAARIT_URL > hôte de la
|
||||
requête > dérivation par l'agent > défaut officiel).
|
||||
"""
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
R = Path(__file__).resolve().parents[1]
|
||||
|
||||
SHA256_CSS_V020 = "abc0c3b2a282566013bd121b0c46b133700f20be78cf44128bebdf19b7e05fab"
|
||||
SHA256_JS_V020 = "24efe399823eed21add535c081ea153ae97afe128744aed9b628cf855e6b50e1"
|
||||
SHA256_LOGO_SHAARIT = "faf0c8a17e7cb82d0884f598eadf2c4fee7a603bfdc3a2ecfdac1e750316db34"
|
||||
|
||||
_ENV_BARRE = (
|
||||
"DRACO_SSO_URL", "DRACO_FLOWDECK_URL", "DRACO_TIKTAK_URL",
|
||||
"DRACO_SHAARIT_URL", "KANBANTE_PUBLIC_URL", "AGENT_URL",
|
||||
"AGENT_EVENEMENTS_URL",
|
||||
)
|
||||
|
||||
|
||||
def _sha256(chemin):
|
||||
return hashlib.sha256((R / chemin).read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def _nettoyer(monkeypatch):
|
||||
for nom in _ENV_BARRE:
|
||||
monkeypatch.delenv(nom, raising=False)
|
||||
|
||||
|
||||
def test_barre_css_js_verbatim_commons_v020():
|
||||
# Remplacé en 0.45.9 : la re-vendange v0.3.0 (HUB-1) supersède
|
||||
# les empreintes v0.2.0 — les empreintes courantes sont
|
||||
# verrouillées par tests/test_barre_v030.py. On garde ici la
|
||||
# preuve que les fichiers ont bien quitté la v0.2.0.
|
||||
assert _sha256("static/css/draco-barre.css") != SHA256_CSS_V020
|
||||
assert _sha256("static/js/draco-barre.js") != SHA256_JS_V020
|
||||
|
||||
|
||||
def test_logo_shaarit_verbatim():
|
||||
assert _sha256("static/img/logos/shaarit.svg") == SHA256_LOGO_SHAARIT
|
||||
|
||||
|
||||
def test_garde_data_theme_apportee_par_la_v020():
|
||||
css = (R / "static/css/draco-barre.css").read_text(encoding="utf-8")
|
||||
# Les deux blocs système sont gardés ; le sombre forcé reste.
|
||||
assert "html:not([data-theme]) .draco-bar" in css
|
||||
assert "html:not([data-theme]) .draco-bar .draco-brand-mark img" in css
|
||||
assert '[data-theme="dark"] .draco-bar' in css
|
||||
assert "@media (prefers-color-scheme: dark) {\n .draco-bar" not in css
|
||||
|
||||
|
||||
def test_url_shaarit_defaut_officiel(monkeypatch):
|
||||
from app.config import _env_settings, resoudre_urls_draco
|
||||
|
||||
_nettoyer(monkeypatch)
|
||||
assert _env_settings().shaarit_url == "https://shaarit.dracodev.net"
|
||||
assert resoudre_urls_draco().shaarit_url == "https://shaarit.dracodev.net"
|
||||
|
||||
|
||||
def test_url_shaarit_derivee_du_seul_agent_evenements(monkeypatch):
|
||||
from app.config import _env_settings
|
||||
|
||||
_nettoyer(monkeypatch)
|
||||
monkeypatch.setenv(
|
||||
"AGENT_EVENEMENTS_URL",
|
||||
"https://agent-dev.dracodev.net/api/v1/evenements",
|
||||
)
|
||||
assert _env_settings().shaarit_url == "https://shaarit-dev.dracodev.net"
|
||||
|
||||
|
||||
def test_url_shaarit_explicite_prioritaire(monkeypatch):
|
||||
from app.config import _env_settings
|
||||
|
||||
_nettoyer(monkeypatch)
|
||||
monkeypatch.setenv(
|
||||
"AGENT_EVENEMENTS_URL",
|
||||
"https://agent-dev.dracodev.net/api/v1/evenements",
|
||||
)
|
||||
monkeypatch.setenv("DRACO_SHAARIT_URL", "https://signets.example.test")
|
||||
assert _env_settings().shaarit_url == "https://signets.example.test"
|
||||
|
||||
|
||||
def test_url_shaarit_par_hote_de_requete(monkeypatch):
|
||||
from app.config import resoudre_urls_draco, suffixe_environnement_requete
|
||||
|
||||
_nettoyer(monkeypatch)
|
||||
assert suffixe_environnement_requete("shaarit-dev.dracodev.net") == "-dev"
|
||||
urls = resoudre_urls_draco("kanbante-dev.dracodev.net")
|
||||
assert urls.shaarit_url == "https://shaarit-dev.dracodev.net"
|
||||
urls = resoudre_urls_draco("kanbante.dracodev.net")
|
||||
assert urls.shaarit_url == "https://shaarit.dracodev.net"
|
||||
|
||||
|
||||
def test_onglet_shaarit_dans_le_rendu(client):
|
||||
r = client.get("/")
|
||||
assert r.status_code == 200
|
||||
assert 'data-app="shaarit"' in r.text
|
||||
assert "https://shaarit.dracodev.net/" in r.text
|
||||
assert "/static/img/logos/shaarit.svg" in r.text
|
||||
# Ordre : Kanbante < ShaarIt < Console Spark (agent).
|
||||
assert r.text.index('data-app="kanbante"') < r.text.index('data-app="shaarit"')
|
||||
assert r.text.index('data-app="shaarit"') < r.text.index('data-app="agent"')
|
||||
assert client.get("/static/img/logos/shaarit.svg").status_code == 200
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Barre commune v0.3.0 (HUB-1, fusion #115) — re-vendange 0.45.10.
|
||||
|
||||
- CSS/JS vendangés VERBATIM depuis dracodev-commons, branche
|
||||
hub-menu-compte-v0-3-0 (PR commons #9), ui/barre-apps/.
|
||||
- Menu compte : liens Hub (Télécharger, À Propos, Aides) et
|
||||
Profil devenu lien vers <hub>/profil. Point explicite du
|
||||
chantier : Kanbante traitait l'action relayée « profil » vers
|
||||
sa page /profile propre — ce comportement est REMPLACÉ par le
|
||||
lien Hub (un seul Profil, celui du Hub). La branche
|
||||
onAction("profil") du gabarit est retirée ; la page /profile
|
||||
elle-même continue d'exister. « Déconnexion » reste relayée.
|
||||
- URL Hub résolue par le même mécanisme que les autres apps
|
||||
(explicite DRACO_HUB_URL > hôte de la requête > dérivation par
|
||||
l'agent > défaut officiel), exposée au gabarit (draco_urls.hub)
|
||||
et passée en urls.hub de DRACO_BAR_CONFIG.
|
||||
"""
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
R = Path(__file__).resolve().parents[1]
|
||||
|
||||
# Empreintes SHA-256 de la source commons v0.3.0
|
||||
# (Projets/dracodev-commons, ui/barre-apps/, branche
|
||||
# hub-menu-compte-v0-3-0 @ff92ece).
|
||||
SHA256_CSS_V030 = "4ac2ed2ee04549f40c71e5700149271d5b07b04e4ab88073e94328fcbf9475b0"
|
||||
SHA256_JS_V030 = "9d84617bfb4346d6a17d769da83d04ebc9bc38d40962c58dd7347f13a6c74da5"
|
||||
|
||||
_ENV_BARRE = (
|
||||
"DRACO_SSO_URL", "DRACO_FLOWDECK_URL", "DRACO_TIKTAK_URL",
|
||||
"DRACO_SHAARIT_URL", "DRACO_HUB_URL", "KANBANTE_PUBLIC_URL",
|
||||
"AGENT_URL", "AGENT_EVENEMENTS_URL",
|
||||
)
|
||||
|
||||
|
||||
def _sha256(chemin):
|
||||
return hashlib.sha256((R / chemin).read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def _nettoyer(monkeypatch):
|
||||
for nom in _ENV_BARRE:
|
||||
monkeypatch.delenv(nom, raising=False)
|
||||
|
||||
|
||||
def test_barre_css_js_verbatim_commons_v030():
|
||||
# Remplacé en 0.45.11 : la re-vendange v0.4.0 (HUB-11)
|
||||
# supersède les empreintes v0.3.0 — les empreintes courantes
|
||||
# sont verrouillées par tests/test_barre_v040.py. On garde ici
|
||||
# la preuve que les fichiers ont bien quitté la v0.3.0.
|
||||
assert _sha256("static/css/draco-barre.css") != SHA256_CSS_V030
|
||||
assert _sha256("static/js/draco-barre.js") != SHA256_JS_V030
|
||||
|
||||
|
||||
def test_js_liens_hub_et_profil_lien():
|
||||
js = (R / "static/js/draco-barre.js").read_text(encoding="utf-8")
|
||||
assert 'hub: "https://hub.dracodev.net/"' in js
|
||||
assert 'hub: "https://hub-dev.dracodev.net/"' in js
|
||||
assert '"telecharger", "Télécharger"' in js
|
||||
assert '"a-propos", "À Propos"' in js
|
||||
assert '"aide", "Aides"' in js
|
||||
assert '"profil", "Profil"' in js
|
||||
assert "data-hub-link" in js
|
||||
assert 'data-compte-act", "profil"' not in js
|
||||
assert 'data-compte-act", "deconnexion"' in js
|
||||
|
||||
|
||||
def test_gabarit_branche_hub_et_retire_profil_relaye():
|
||||
gabarit = (R / "app/templates/_draco_bar.html").read_text(encoding="utf-8")
|
||||
assert 'urls: { hub: "{{ draco_urls.hub }}/" }' in gabarit
|
||||
# La branche relayée vers /profile a disparu du onAction…
|
||||
assert 'if (act === "profil")' not in gabarit
|
||||
# …mais la déconnexion relayée est intacte.
|
||||
assert 'if (act === "deconnexion") { location.href = "/logout"; return; }' \
|
||||
in gabarit
|
||||
|
||||
|
||||
def test_hub_resolution_meme_mecanisme(monkeypatch):
|
||||
_nettoyer(monkeypatch)
|
||||
from app.config import resoudre_urls_draco
|
||||
assert resoudre_urls_draco().hub_url == "https://hub.dracodev.net"
|
||||
assert resoudre_urls_draco("kanbante-dev.dracodev.net").hub_url == \
|
||||
"https://hub-dev.dracodev.net"
|
||||
assert resoudre_urls_draco("kanbante.dracodev.net").hub_url == \
|
||||
"https://hub.dracodev.net"
|
||||
monkeypatch.setenv("DRACO_HUB_URL", "https://hub.example.net/")
|
||||
assert resoudre_urls_draco("kanbante-dev.dracodev.net").hub_url == \
|
||||
"https://hub.example.net"
|
||||
|
||||
|
||||
def test_page_profile_existe_toujours():
|
||||
# La page propre à Kanbante n'est pas supprimée : seul le
|
||||
# menu compte de la barre ne la cible plus.
|
||||
main = (R / "app/main.py").read_text(encoding="utf-8")
|
||||
assert '@app.get("/profile")' in main
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Barre commune v0.4.0 (HUB-11, fusion #125) — re-vendange 0.45.11.
|
||||
|
||||
- CSS/JS vendangés VERBATIM depuis dracodev-commons, branche
|
||||
menu-compte-icones-v0-4-0 (PR commons #10), ui/barre-apps/.
|
||||
- Menu compte : chaque item (Télécharger, À Propos, Aides,
|
||||
Profil, Déconnexion) porte une icône SVG monoline construite
|
||||
en DOM (contour, currentColor, sans remplissage), dans le
|
||||
style des icônes du rail de la console — aucun emoji.
|
||||
L'en-tête d'identité reste sans icône ; les liens Hub et le
|
||||
relais « Déconnexion » sont inchangés depuis la v0.3.0.
|
||||
"""
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
R = Path(__file__).resolve().parents[1]
|
||||
|
||||
# Empreintes SHA-256 de la source commons v0.4.0
|
||||
# (Projets/dracodev-commons, ui/barre-apps/, branche
|
||||
# menu-compte-icones-v0-4-0 @e8ca99a).
|
||||
SHA256_CSS_V040 = "d8cf53b2f6700ebc7499d43869d5cae256d1f74636f0113bffb174b8a6bd06ec"
|
||||
SHA256_JS_V040 = "36cc24b2bfea757633a7cbcfded54fb73405d9f6fdbde1b4a8fb8df7ae5d39a0"
|
||||
|
||||
|
||||
def _sha256(chemin):
|
||||
return hashlib.sha256((R / chemin).read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def test_barre_css_js_verbatim_commons_v040():
|
||||
# Remplacé en 0.45.12 : la re-vendange v0.4.1 (correctif
|
||||
# fusion #125) supersède les empreintes v0.4.0 — les empreintes
|
||||
# courantes sont verrouillées par tests/test_barre_v041.py.
|
||||
# Le CSS a lui aussi quitté la v0.4.0 : la v0.4.1 le resserre
|
||||
# (menu compte 248 → 232 px, @edde1b5) ; on garde ici la
|
||||
# preuve que les deux fichiers ont bien quitté la v0.4.0.
|
||||
assert _sha256("static/css/draco-barre.css") != SHA256_CSS_V040
|
||||
assert _sha256("static/js/draco-barre.js") != SHA256_JS_V040
|
||||
|
||||
|
||||
def test_js_icones_monoline_menu_compte():
|
||||
js = (R / "static/js/draco-barre.js").read_text(encoding="utf-8")
|
||||
assert "MENU_ICONES" in js
|
||||
for nom in ("telecharger", "a-propos", "aide", "profil", "deconnexion"):
|
||||
assert f'"{nom}"' in js
|
||||
assert "draco-menu-icone" in js
|
||||
assert 'setAttribute("stroke", "currentColor")' in js
|
||||
assert 'setAttribute("fill", "none")' in js
|
||||
# Comportements v0.3.0 conservés.
|
||||
assert 'hub: "https://hub.dracodev.net/"' in js
|
||||
assert '"profil", "Profil"' in js
|
||||
assert 'data-compte-act", "deconnexion"' in js
|
||||
|
||||
|
||||
def test_css_icone_menu_compte():
|
||||
css = (R / "static/css/draco-barre.css").read_text(encoding="utf-8")
|
||||
assert ".draco-menu-icone" in css
|
||||
bloc = css.split(".draco-menu-icone {", 1)[1].split("}", 1)[0]
|
||||
assert "width: 18px" in bloc
|
||||
assert "var(--ink-2)" in bloc
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Barre commune v0.4.1 (correctif fusion #125) — re-vendange 0.45.12.
|
||||
|
||||
- JS vendangé VERBATIM depuis dracodev-commons, branche
|
||||
menu-compte-icones-svg-ns-v0-4-1 (PR commons #11),
|
||||
ui/barre-apps/. Le CSS est lui aussi verbatim : la source
|
||||
commons v0.4.1 y ajoute le resserrement du menu compte
|
||||
(min-width 248 → 232 px, demande de Bruno, @edde1b5).
|
||||
- Correctif : en v0.4.0, les icônes du menu compte étaient
|
||||
créées via document.createElement() — espace de noms HTML,
|
||||
jamais dessinées par le navigateur (libellés décalés d'un
|
||||
cran d'icône, aucun glyphe visible en production). La v0.4.1
|
||||
crée le <svg> et ses primitives via
|
||||
document.createElementNS() (espace de noms SVG).
|
||||
"""
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
R = Path(__file__).resolve().parents[1]
|
||||
|
||||
# Empreintes SHA-256 de la source commons
|
||||
# (Projets/dracodev-commons, ui/barre-apps/ ; branche du
|
||||
# correctif v0.4.1 @edde1b5 : JS espace de noms SVG, CSS
|
||||
# menu compte resserré à 232 px).
|
||||
SHA256_CSS_V041 = "874b26eafba5b3e174140280415edbae00cd38bc4beb39d897a4b4c5b923af6e"
|
||||
SHA256_JS_V041 = "82c4940e8e115ed71d95f7c0dcdc906b93e49855580cc0fec7a1ccba34f85a42"
|
||||
|
||||
|
||||
def _sha256(chemin):
|
||||
return hashlib.sha256((R / chemin).read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def test_barre_css_js_verbatim_commons_v041():
|
||||
assert _sha256("static/css/draco-barre.css") == SHA256_CSS_V041
|
||||
assert _sha256("static/js/draco-barre.js") == SHA256_JS_V041
|
||||
|
||||
|
||||
def test_js_icones_espace_de_noms_svg():
|
||||
js = (R / "static/js/draco-barre.js").read_text(encoding="utf-8")
|
||||
assert "MENU_ICONES" in js
|
||||
# Le correctif : création dans l'espace de noms SVG…
|
||||
assert 'createElementNS(SVG_NS, "svg")' in js
|
||||
assert "createElementNS(SVG_NS, spec[0])" in js
|
||||
# …et plus aucune création HTML du svg ou des primitives.
|
||||
assert 'createElement("svg")' not in js
|
||||
assert "createElement(spec[0])" not in js
|
||||
# Comportements conservés.
|
||||
assert 'hub: "https://hub.dracodev.net/"' in js
|
||||
assert '"profil", "Profil"' in js
|
||||
assert 'data-compte-act", "deconnexion"' in js
|
||||
@@ -198,7 +198,7 @@ def test_menu_des_applications_par_la_marque(client):
|
||||
js = client.get("/static/js/draco-barre.js").text
|
||||
assert "draco-apps-menu" in js
|
||||
css = client.get("/static/css/draco-barre.css").text
|
||||
assert "font-size: 24px" in css
|
||||
assert ".draco-brand-mark img" in css # v0.1.10 : la marque est le logo Spark (26 px), plus de glyphe 24 px
|
||||
|
||||
|
||||
def test_barre_pleine_largeur_et_palier_mobile(client):
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
"""HUB-16 (0.46.0) — correspondance draco PAR UTILISATEUR.
|
||||
|
||||
Le pont SSO ne désigne plus « l'opérateur » : chaque identité
|
||||
draco (claim ``sub``) désigne SON compte Kanbante, via la colonne
|
||||
users.draco_sub. Couvert ici :
|
||||
|
||||
- amorçage : l'unique compte admin sans liaison se lie au
|
||||
premier passage (le compte existant garde son accès) ;
|
||||
- JIT : deux identités distinctes → deux comptes distincts,
|
||||
rôle membre, mot de passe local non divulgué/inutilisable ;
|
||||
- le JIT n'adopte jamais un compte préexistant non lié, même
|
||||
homonyme : il crée un compte nouveau, suffixé ;
|
||||
- liaison / déliaison par l'administrateur depuis /admin,
|
||||
conflit de sub refusé ;
|
||||
- compte désactivé : correspondance refusée, aucun contournement ;
|
||||
- /api/search : un jeton draco sans correspondance ne suffit plus,
|
||||
un jeton lié passe ;
|
||||
- non-régression : un jeton personnel kbt_ reste accepté sur les
|
||||
surfaces machine pendant que l'authentification est active.
|
||||
|
||||
La base de test est partagée par toute la session (voir
|
||||
conftest) : les comptes créés ici portent des noms/subs uniques
|
||||
« hub16-… » et sont supprimés au teardown de chaque test.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import app.main as main
|
||||
from app import accounts, draco, models
|
||||
|
||||
SECRET = "secret-partage-hub16"
|
||||
|
||||
SUBS_TEST = (
|
||||
"hub16-alice", "hub16-bob", "hub16-carole", "hub16-dave",
|
||||
"hub16-erin", "hub16-franck", "hub16-amorce", "hub16-sans-lien",
|
||||
"hub16-recherche",
|
||||
)
|
||||
|
||||
|
||||
def _jeton(sub, nom="Alice Exemple", secret=SECRET):
|
||||
def b64(d: bytes) -> str:
|
||||
return base64.urlsafe_b64encode(d).rstrip(b"=").decode()
|
||||
|
||||
header = b64(json.dumps({"alg": "HS256", "typ": "JWT"}).encode())
|
||||
payload = b64(json.dumps({
|
||||
"sub": sub, "name": nom, "iat": int(time.time()),
|
||||
"exp": int(time.time()) + 3600, "jti": f"jti-{sub}",
|
||||
}).encode())
|
||||
sig = hmac.new(secret.encode(), f"{header}.{payload}".encode(),
|
||||
hashlib.sha256).digest()
|
||||
return f"{header}.{payload}.{b64(sig)}"
|
||||
|
||||
|
||||
def _nettoyer():
|
||||
"""Supprime les comptes créés par ces tests (et leurs traces)."""
|
||||
with models.get_db() as conn:
|
||||
ids = [
|
||||
r[0] for r in conn.execute(
|
||||
"SELECT id FROM users WHERE draco_sub IN ({})".format(
|
||||
",".join("?" * len(SUBS_TEST))
|
||||
),
|
||||
SUBS_TEST,
|
||||
).fetchall()
|
||||
]
|
||||
ids += [
|
||||
r[0] for r in conn.execute(
|
||||
"SELECT id FROM users WHERE username IN"
|
||||
" ('hub16-dave', 'hub16-erin', 'carole-exemple')"
|
||||
).fetchall()
|
||||
]
|
||||
for uid in ids:
|
||||
conn.execute("DELETE FROM api_tokens WHERE user_id = ?", (uid,))
|
||||
conn.execute(
|
||||
"DELETE FROM workspace_members WHERE user_id = ?", (uid,)
|
||||
)
|
||||
conn.execute("DELETE FROM users WHERE id = ?", (uid,))
|
||||
|
||||
|
||||
def _restaurer_temoin():
|
||||
"""Délie le compte témoin d'amorçage (jamais supprimé)."""
|
||||
temoin = models.get_user_by_draco("hub16-temoin-amorce")
|
||||
if temoin is not None:
|
||||
models.update_user(temoin["id"], draco_sub=None)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client_hub16(monkeypatch, fake_gitea):
|
||||
monkeypatch.setenv("KANBANTE_AUTH_USER", "bruno")
|
||||
monkeypatch.setenv("KANBANTE_AUTH_PASSWORD", "motdepasse")
|
||||
monkeypatch.setenv("DRACO_SESSION_SECRET", SECRET)
|
||||
monkeypatch.setattr(draco, "_refresh_revocations", lambda: None)
|
||||
draco._revoked_cache.clear()
|
||||
_restaurer_temoin()
|
||||
_nettoyer()
|
||||
with TestClient(main.app) as client:
|
||||
yield client
|
||||
_restaurer_temoin()
|
||||
_nettoyer()
|
||||
draco._revoked_cache.clear()
|
||||
|
||||
|
||||
def _pont(client, sub, nom="Alice Exemple"):
|
||||
client.cookies.set("draco_session", _jeton(sub, nom))
|
||||
return client.get("/auth/draco", follow_redirects=False)
|
||||
|
||||
|
||||
def _profil_username(client) -> str:
|
||||
r = client.get("/profile")
|
||||
assert r.status_code == 200
|
||||
# « <username> · rôle : … » dans l'en-tête de la page profil.
|
||||
return r.text.split("page-sub\">", 1)[1].split(" ·", 1)[0].strip()
|
||||
|
||||
|
||||
def _unique_admin_sans_liaison():
|
||||
"""(compte, vrai) si l'état partagé est le cas d'amorçage pur."""
|
||||
users = models.list_users()
|
||||
if len(users) == 1 and users[0]["role"] == "admin" \
|
||||
and not users[0].get("draco_sub"):
|
||||
return users[0], True
|
||||
return None, False
|
||||
|
||||
|
||||
# ── Amorçage ──────────────────────────────────────────────────
|
||||
|
||||
def test_amorcage_unique_admin_se_lie_au_premier_passage(client_hub16):
|
||||
admin, pur = _unique_admin_sans_liaison()
|
||||
if not pur:
|
||||
pytest.skip("base partagée : le cas d'amorçage pur n'est pas en place")
|
||||
avant = models.count_users()
|
||||
r = _pont(client_hub16, "hub16-amorce", "Bruno")
|
||||
assert r.status_code == 303 and r.headers["location"] == "/"
|
||||
assert "kanbante_session" in r.headers.get("set-cookie", "")
|
||||
# Aucun compte créé : le sub s'est lié au compte admin existant.
|
||||
assert models.count_users() == avant
|
||||
lie = models.get_user_by_draco("hub16-amorce")
|
||||
assert lie is not None and lie["id"] == admin["id"]
|
||||
assert lie["role"] == "admin"
|
||||
assert _profil_username(client_hub16) == admin["username"]
|
||||
# Nettoyage propre à ce test : rendre l'amorçage à son état.
|
||||
models.update_user(admin["id"], draco_sub=None)
|
||||
|
||||
|
||||
# ── Création JIT ──────────────────────────────────────────────
|
||||
|
||||
def test_jit_deux_identites_deux_comptes_distincts(client_hub16):
|
||||
admin, pur = _unique_admin_sans_liaison()
|
||||
if pur:
|
||||
# Neutraliser l'amorçage : les scénarios JIT doivent être purs.
|
||||
models.update_user(admin["id"], draco_sub="hub16-temoin-amorce")
|
||||
try:
|
||||
r = _pont(client_hub16, "hub16-alice", "Alice Exemple")
|
||||
assert r.status_code == 303 and r.headers["location"] == "/"
|
||||
assert _profil_username(client_hub16) == "alice-exemple"
|
||||
alice = models.get_user_by_draco("hub16-alice")
|
||||
assert alice is not None
|
||||
assert alice["username"] == "alice-exemple"
|
||||
assert alice["display_name"] == "Alice Exemple"
|
||||
assert alice["role"] == "member"
|
||||
# Mot de passe local aléatoire : ni le mot de passe
|
||||
# d'environnement ni le nom ne l'ouvrent.
|
||||
assert accounts.verify_password("motdepasse", alice["password_hash"]) is False
|
||||
assert alice["password_hash"].startswith("pbkdf2$")
|
||||
|
||||
client_hub16.cookies.delete("kanbante_session")
|
||||
r = _pont(client_hub16, "hub16-bob", "Bob Exemple")
|
||||
assert r.status_code == 303 and r.headers["location"] == "/"
|
||||
assert _profil_username(client_hub16) == "bob-exemple"
|
||||
bob = models.get_user_by_draco("hub16-bob")
|
||||
assert bob is not None and bob["id"] != alice["id"]
|
||||
|
||||
# Deuxième passage d'Alice : MÊME compte, pas de doublon.
|
||||
client_hub16.cookies.delete("kanbante_session")
|
||||
avant = models.count_users()
|
||||
_pont(client_hub16, "hub16-alice", "Alice Exemple")
|
||||
assert models.count_users() == avant
|
||||
assert models.get_user_by_draco("hub16-alice")["id"] == alice["id"]
|
||||
assert _profil_username(client_hub16) == "alice-exemple"
|
||||
finally:
|
||||
if pur:
|
||||
models.update_user(admin["id"], draco_sub=None)
|
||||
|
||||
|
||||
def test_jit_n_adopte_pas_un_compte_homonyme_non_lie(client_hub16):
|
||||
admin, pur = _unique_admin_sans_liaison()
|
||||
if pur:
|
||||
models.update_user(admin["id"], draco_sub="hub16-temoin-amorce")
|
||||
try:
|
||||
local_id = models.create_user(
|
||||
"carole-exemple", accounts.hash_password("local-carole"),
|
||||
role="member", display_name="Carole Exemple",
|
||||
)
|
||||
r = _pont(client_hub16, "hub16-carole", "Carole Exemple")
|
||||
assert r.status_code == 303 and r.headers["location"] == "/"
|
||||
# Le compte préexistant n'a PAS été lié ni utilisé…
|
||||
local = models.get_user(local_id)
|
||||
assert local["draco_sub"] is None
|
||||
# …un compte nouveau, suffixé, porte la correspondance.
|
||||
cree = models.get_user_by_draco("hub16-carole")
|
||||
assert cree is not None and cree["id"] != local_id
|
||||
assert cree["username"] == "carole-exemple-2"
|
||||
assert _profil_username(client_hub16) == "carole-exemple-2"
|
||||
finally:
|
||||
if pur:
|
||||
models.update_user(admin["id"], draco_sub=None)
|
||||
|
||||
|
||||
def test_compte_desactive_refuse_sans_contournement(client_hub16):
|
||||
admin, pur = _unique_admin_sans_liaison()
|
||||
if pur:
|
||||
models.update_user(admin["id"], draco_sub="hub16-temoin-amorce")
|
||||
try:
|
||||
_pont(client_hub16, "hub16-franck", "Franck Exemple")
|
||||
franck = models.get_user_by_draco("hub16-franck")
|
||||
assert franck is not None
|
||||
models.update_user(franck["id"], active=0)
|
||||
client_hub16.cookies.delete("kanbante_session")
|
||||
avant = models.count_users()
|
||||
r = _pont(client_hub16, "hub16-franck", "Franck Exemple")
|
||||
# Refusé au login, aucune session, aucun nouveau compte.
|
||||
assert r.headers["location"] == "/login"
|
||||
assert "kanbante_session" not in r.headers.get("set-cookie", "")
|
||||
assert models.count_users() == avant
|
||||
finally:
|
||||
if pur:
|
||||
models.update_user(admin["id"], draco_sub=None)
|
||||
|
||||
|
||||
# ── Liaison par l'administrateur ──────────────────────────────
|
||||
|
||||
def _jeton_csrf_via_login(client) -> str:
|
||||
"""Charge la page de connexion (elle pose le cookie CSRF).
|
||||
|
||||
csrf_from() charge « / », qui répond 401 JSON sans cookie
|
||||
quand l'authentification est active et qu'aucune session
|
||||
n'est ouverte — la page /login, elle, est rendue (render()
|
||||
pose le cookie) dans tous les cas.
|
||||
"""
|
||||
client.get("/login")
|
||||
return client.cookies.get("kanbante_csrf") or ""
|
||||
|
||||
|
||||
def _connecter_admin(client):
|
||||
token = _jeton_csrf_via_login(client)
|
||||
r = client.post(
|
||||
"/login",
|
||||
data={"username": "bruno", "password": "motdepasse",
|
||||
"next": "/", "csrf_token": token},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert r.status_code == 303, r.text
|
||||
assert "kanbante_session" in r.headers.get("set-cookie", "")
|
||||
|
||||
|
||||
def test_liaison_par_administrateur(client_hub16):
|
||||
dave_id = models.create_user(
|
||||
"hub16-dave", accounts.hash_password("local-dave"),
|
||||
role="member", display_name="Dave",
|
||||
)
|
||||
erin_id = models.create_user(
|
||||
"hub16-erin", accounts.hash_password("local-erin"),
|
||||
role="member", display_name="Erin",
|
||||
)
|
||||
_connecter_admin(client_hub16)
|
||||
# L'écran d'administration expose le mécanisme de liaison.
|
||||
r = client_hub16.get("/admin")
|
||||
assert r.status_code == 200
|
||||
assert "/draco" in r.text and "Lier" in r.text
|
||||
|
||||
# Lier Dave au sub hub16-dave.
|
||||
r = client_hub16.post(
|
||||
f"/admin/users/{dave_id}/draco",
|
||||
data={"draco_sub": "hub16-dave",
|
||||
"csrf_token": client_hub16.cookies.get("kanbante_csrf")},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert r.status_code == 303 and r.headers["location"].startswith("/admin?msg=")
|
||||
assert models.get_user(dave_id)["draco_sub"] == "hub16-dave"
|
||||
|
||||
# Conflit : le même sub pour Erin est refusé, Erin inchangée.
|
||||
r = client_hub16.post(
|
||||
f"/admin/users/{erin_id}/draco",
|
||||
data={"draco_sub": "hub16-dave",
|
||||
"csrf_token": client_hub16.cookies.get("kanbante_csrf")},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert r.status_code == 303 and "err=" in r.headers["location"]
|
||||
assert models.get_user(erin_id)["draco_sub"] is None
|
||||
|
||||
# Le pont route désormais ce sub vers Dave, et seulement lui.
|
||||
client_hub16.cookies.delete("kanbante_session")
|
||||
r = _pont(client_hub16, "hub16-dave", "Dave")
|
||||
assert r.status_code == 303 and r.headers["location"] == "/"
|
||||
assert _profil_username(client_hub16) == "hub16-dave"
|
||||
|
||||
# Déliaison par l'administrateur (champ vide).
|
||||
_connecter_admin(client_hub16)
|
||||
r = client_hub16.post(
|
||||
f"/admin/users/{dave_id}/draco",
|
||||
data={"draco_sub": "",
|
||||
"csrf_token": client_hub16.cookies.get("kanbante_csrf")},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert r.status_code == 303
|
||||
assert models.get_user(dave_id)["draco_sub"] is None
|
||||
|
||||
|
||||
def test_liaison_refusee_aux_non_admins(client_hub16):
|
||||
dave_id = models.create_user(
|
||||
"hub16-dave", accounts.hash_password("local-dave"),
|
||||
role="member", display_name="Dave",
|
||||
)
|
||||
# Connexion native de Dave (membre) : aucun droit d'administration.
|
||||
token = _jeton_csrf_via_login(client_hub16)
|
||||
r = client_hub16.post(
|
||||
"/login",
|
||||
data={"username": "hub16-dave", "password": "local-dave",
|
||||
"next": "/", "csrf_token": token},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert r.status_code == 303
|
||||
r = client_hub16.post(
|
||||
f"/admin/users/{dave_id}/draco",
|
||||
data={"draco_sub": "hub16-dave",
|
||||
"csrf_token": client_hub16.cookies.get("kanbante_csrf")},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert r.status_code == 403
|
||||
assert models.get_user(dave_id)["draco_sub"] is None
|
||||
|
||||
|
||||
# ── /api/search par utilisateur ───────────────────────────────
|
||||
|
||||
def test_api_search_draco_par_utilisateur(client_hub16):
|
||||
admin, pur = _unique_admin_sans_liaison()
|
||||
if pur:
|
||||
models.update_user(admin["id"], draco_sub="hub16-temoin-amorce")
|
||||
try:
|
||||
# Jeton draco valide mais SANS correspondance : refusé
|
||||
# (il ne désigne plus l'opérateur).
|
||||
client_hub16.cookies.set(
|
||||
"draco_session", _jeton("hub16-sans-lien", "Sans Lien")
|
||||
)
|
||||
r = client_hub16.get("/api/search", params={"q": "demo"})
|
||||
assert r.status_code == 401
|
||||
# Après création JIT par le pont, le même chemin passe.
|
||||
_pont(client_hub16, "hub16-recherche", "Recherche Exemple")
|
||||
client_hub16.cookies.delete("kanbante_session")
|
||||
client_hub16.cookies.set(
|
||||
"draco_session", _jeton("hub16-recherche", "Recherche Exemple")
|
||||
)
|
||||
r = client_hub16.get("/api/search", params={"q": "demo"})
|
||||
assert r.status_code == 200
|
||||
assert r.json()["query"] == "demo"
|
||||
finally:
|
||||
if pur:
|
||||
models.update_user(admin["id"], draco_sub=None)
|
||||
|
||||
|
||||
# ── Non-régression : jetons kbt_ ──────────────────────────────
|
||||
|
||||
def test_jeton_kbt_toujours_accepte(client_hub16):
|
||||
uid = models.create_user(
|
||||
"hub16-erin", accounts.hash_password("local-erin"),
|
||||
role="member", display_name="Erin Jetons",
|
||||
)
|
||||
user = models.get_user(uid)
|
||||
_row, clair = models.create_api_token(uid, "service-agents")
|
||||
assert clair.startswith("kbt_")
|
||||
corps = {"jsonrpc": "2.0", "id": 1, "method": "initialize",
|
||||
"params": {"protocolVersion": "2024-11-05"}}
|
||||
# Sans jeton : 401 ; avec le jeton Bearer : le handshake passe.
|
||||
assert client_hub16.post("/mcp", json=corps).status_code == 401
|
||||
r = client_hub16.post(
|
||||
"/mcp", json=corps, headers={"Authorization": f"Bearer {clair}"}
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.json()["result"]["serverInfo"]["name"] == "kanbante"
|
||||
# Le jeton sert aussi /api/search (surface machine)…
|
||||
r = client_hub16.get(
|
||||
"/api/search", params={"q": "demo"},
|
||||
headers={"Authorization": f"Bearer {clair}"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
# …mais jamais l'interface web.
|
||||
assert client_hub16.get(
|
||||
"/", headers={"Authorization": f"Bearer {clair}",
|
||||
"Accept": "application/json"},
|
||||
).status_code == 401
|
||||
assert user["username"] == "hub16-erin"
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
Le cookie émis par ObsiGate (ACC-3.1) est vérifié localement
|
||||
(secret partagé, HS256) et converti en session Kanbante native par
|
||||
GET /auth/draco. Kanbante est mono-utilisateur : un jeton valide
|
||||
ouvre la session de l'opérateur configuré. Sans
|
||||
DRACO_SESSION_SECRET, le pont renvoie simplement au login.
|
||||
GET /auth/draco. Depuis la 0.46.0 (HUB-16), la correspondance est
|
||||
par utilisateur (users.draco_sub, création JIT) — voir
|
||||
tests/test_draco_correspondance.py. Les cas couverts ici restent
|
||||
valides : dans ce module, le seul compte est l'admin d'amorçage,
|
||||
auquel le premier jeton valide se lie (cas d'amorçage documenté).
|
||||
Sans DRACO_SESSION_SECRET, le pont renvoie simplement au login.
|
||||
"""
|
||||
|
||||
import base64
|
||||
|
||||
@@ -16,8 +16,10 @@ def test_sidebar_charbon_et_accent_vert(client, fake_gitea):
|
||||
def test_nav_icons_sont_des_svg(client, fake_gitea):
|
||||
page = client.get("/")
|
||||
assert 'class="side-ico"><svg' in page.text
|
||||
# Le logo Kanbante est aussi un SVG (colonnes blanches).
|
||||
assert 'class="logo-mark"><svg' in page.text
|
||||
# Le logo de la barre latérale est le logo officiel
|
||||
# (kanbante.png, celui de la barre d'apps commune) —
|
||||
# remplacement demandé par Bruno le 2026-10-30.
|
||||
assert 'class="logo-mark"><img src="/static/img/logos/kanbante.png"' in page.text
|
||||
|
||||
|
||||
def test_theme_toggle_dans_admin_pas_dans_en_tete(client, fake_gitea):
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Retours de Bruno (2026-10-30) : logo de la barre latérale,
|
||||
board en thème clair, badge d'âge illisible en clair.
|
||||
|
||||
1. La barre latérale montrait un pictogramme « 3 barres » ;
|
||||
le logo doit être celui de la barre d'apps commune
|
||||
(static/img/logos/kanbante.png).
|
||||
2. En thème clair, les colonnes du board et le panneau Notes
|
||||
gardaient le fond sombre codé en dur rgba(20,24,34,.72).
|
||||
3. Le badge d'âge (En vol, Focus) : texte jaune pâle sur
|
||||
fond jaune translucide — illisible en thème clair.
|
||||
"""
|
||||
|
||||
|
||||
def test_sidebar_logo_officiel(client, fake_gitea):
|
||||
page = client.get("/")
|
||||
assert 'src="/static/img/logos/kanbante.png"' in page.text
|
||||
# L'ancien pictogramme à barres blanches a disparu.
|
||||
assert '<rect x="3" y="4" width="5" height="16"' not in page.text
|
||||
|
||||
|
||||
def test_logo_officiel_servi(client, fake_gitea):
|
||||
r = client.get("/static/img/logos/kanbante.png")
|
||||
assert r.status_code == 200
|
||||
assert r.content[:4] == b"\x89PNG"
|
||||
|
||||
|
||||
def test_board_clair_surfaces_eclaircies(client, fake_gitea):
|
||||
css = client.get("/static/css/style.css").text
|
||||
assert (
|
||||
'html[data-theme="light"] .kanban-column '
|
||||
'{ background: rgba(255, 255, 255, 0.85); }'
|
||||
) in css
|
||||
assert (
|
||||
'html[data-theme="light"] .notes-panel '
|
||||
'{ background: rgba(255, 255, 255, 0.85); }'
|
||||
) in css
|
||||
|
||||
|
||||
def test_badge_age_lisible_en_clair(client, fake_gitea):
|
||||
css = client.get("/static/css/style.css").text
|
||||
assert 'html[data-theme="light"] .age-badge { color: #b45309; }' in css
|
||||
assert (
|
||||
'html[data-theme="light"] .age-badge.stale { color: #dc2626; }'
|
||||
) in css
|
||||
assert (
|
||||
'html[data-theme="light"] .column-count.over { color: #dc2626; }'
|
||||
) in css
|
||||
@@ -18,6 +18,7 @@ RESOLVED_DEV = {
|
||||
"flowdeck": "https://flowdeck-dev.dracodev.net",
|
||||
"tiktak": "https://tiktak-dev.dracodev.net/",
|
||||
"kanbante": "https://kanbante-dev.dracodev.net/",
|
||||
"shaarit": "https://shaarit-dev.dracodev.net/",
|
||||
}
|
||||
|
||||
|
||||
@@ -34,17 +35,20 @@ def test_origin_of():
|
||||
|
||||
|
||||
def test_sister_origins_dev():
|
||||
# Correctif 2026-10-28 : 10 origines officielles (8 sœurs + 2 agent), prod ET dev,
|
||||
# quelle que soit la résolution.
|
||||
# Correctif 2026-10-28 : origines officielles prod ET dev, quelle
|
||||
# que soit la résolution ; ShaarIt ajoutée par la fédération
|
||||
# v0.2.1 (12 origines : 10 sœurs + 2 agent).
|
||||
assert sister_origins(RESOLVED_DEV) == {
|
||||
"https://og.dracodev.net",
|
||||
"https://flowdeck.dracodev.net",
|
||||
"https://tiktak.dracodev.net",
|
||||
"https://kanbante.dracodev.net",
|
||||
"https://shaarit.dracodev.net",
|
||||
"https://og-dev.dracodev.net",
|
||||
"https://flowdeck-dev.dracodev.net",
|
||||
"https://tiktak-dev.dracodev.net",
|
||||
"https://kanbante-dev.dracodev.net",
|
||||
"https://shaarit-dev.dracodev.net",
|
||||
"https://agent.dracodev.net",
|
||||
"https://agent-dev.dracodev.net",
|
||||
}
|
||||
@@ -122,3 +126,16 @@ def test_sister_origins_hote_requete():
|
||||
assert "https://og-dev.dracodev.net" in sister_origins(
|
||||
{}, request_host="kanbante.dracodev.net"
|
||||
)
|
||||
|
||||
|
||||
def test_gabarit_recherche_assets_versionnes():
|
||||
"""Fusion #111 (correctif) : les assets de la page /recherche sont
|
||||
versionnés (?v=), comme nav.js et la barre — un recherche.js
|
||||
d'avant la source ShaarIt servi par un cache navigateur laissait
|
||||
la section ShaarIt figée sur « Tapez une recherche ci-dessus »,
|
||||
sans aucun appel (retour d'usage du 2026-11-04)."""
|
||||
from pathlib import Path
|
||||
|
||||
gabarit = Path("app/templates/recherche.html").read_text(encoding="utf-8")
|
||||
assert "/static/js/recherche.js?v={{ version }}" in gabarit
|
||||
assert "/static/css/recherche.css?v={{ version }}" in gabarit
|
||||
|
||||