Compare commits
78
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95b0b219e8 | ||
|
|
14488f662c | ||
|
|
ec0174f9d4 | ||
|
|
687c968f3e | ||
|
|
c8fdf61cf3 | ||
|
|
12e6db7fe2 | ||
|
|
f25862d838 | ||
|
|
dc1f00f24a | ||
|
|
1b8b83c005 | ||
|
|
94900ccd53 | ||
|
|
8719d0d6f8 | ||
|
|
1bc0696d6d | ||
|
|
505a1e1542 | ||
|
|
037f0e0b98 | ||
|
|
4190ee825d | ||
|
|
1eed495342 | ||
|
|
e7ceb14d2b | ||
|
|
f976d22d4b | ||
|
|
a0b461f468 | ||
|
|
c2052959d1 | ||
|
|
e23dd499a9 | ||
|
|
c091189381 | ||
|
|
e49f498ca0 | ||
|
|
9eb236ba63 | ||
|
|
e3a89710d7 | ||
|
|
546e6fb81b | ||
|
|
bcd9c2face | ||
|
|
b4f3cce0df | ||
|
|
0ce5de46f3 | ||
|
|
cb056ee456 | ||
|
|
a01b6a484a | ||
|
|
2735905c7e | ||
|
|
8596ad5708 | ||
|
|
642c7bbb2d | ||
|
|
7a3081c408 | ||
|
|
21414de936 | ||
|
|
bb3da870a0 | ||
|
|
7cf2a3fc25 | ||
|
|
6602a070be | ||
|
|
9e1ff33698 | ||
|
|
47dd9c06a1 | ||
|
|
90b9189169 | ||
|
|
91ddfcd3b3 | ||
|
|
2c10040046 | ||
|
|
32fbe1d137 | ||
|
|
4d1757e3d2 | ||
|
|
9dc62885b0 | ||
|
|
cc007dfa04 | ||
|
|
f0ca8b9ec6 | ||
|
|
6a938cf778 | ||
|
|
c60694bf23 | ||
|
|
a08fa53bab | ||
|
|
1c81259e0d | ||
|
|
5749b1c718 | ||
|
|
c35aafd2e2 | ||
|
|
219c614216 | ||
|
|
0d6499ed1a | ||
|
|
a09ae26d78 | ||
|
|
56f39c0959 | ||
|
|
99a7ebd504 | ||
|
|
e8f48d3009 | ||
|
|
dd6a4dd82c | ||
|
|
a2f3564cc8 | ||
|
|
ea39916db5 | ||
|
|
724d7a7799 | ||
|
|
5ff51d8e2d | ||
|
|
436084aebd | ||
|
|
8ab30d25ef | ||
|
|
6cac4e71f5 | ||
|
|
210f4f8983 | ||
|
|
f150d4e136 | ||
|
|
5787bcf168 | ||
|
|
80006bdf35 | ||
|
|
0219fafa34 | ||
|
|
c97dfb0ef4 | ||
|
|
1b1c793919 | ||
|
|
d11ca0cbff | ||
|
|
0e2df86be5 |
@@ -1,3 +1,53 @@
|
||||
## 0.42.0 — 2026-10-28 — Lot UX complet (#57 à #68)
|
||||
|
||||
- **#57 Jetons** : échelle fermée documentée (typo xs/sm/base/lg/xl, rayons sm/md/base/full, ombres, espacements) ; rayons pilule passés au jeton `--radius-full`.
|
||||
- **#58 Boutons** : système primary uni (fini les dégradés), secondary, danger, icon 36px, états unifiés ; `.btn-save` et `.proj-primary` alignés sur le primary.
|
||||
- **#59 Icônes** : emoji d'interface retirés des gabarits (cartes, tableaux, états vides, boutons, titres) ; épingler en SVG trait, recherche en SVG ; favicons et contenu utilisateur (titres/labels Gitea) inchangés.
|
||||
- **#60 Accessibilité** : jeton `--accent-text` distinct (`#14b8a6` sombre, `#0f766e` clair, 5,47:1 sur blanc) pour tous les textes d'accent ; focus visible unifié.
|
||||
- **#61 Typographie** : pile système assumée (Inter déclarée mais jamais chargée, pas de CDN tiers pour une app auto-hébergée) ; hiérarchie H1/H2 via `.page-title`.
|
||||
- **#62 Pages** : Aujourd'hui et Focus reprennent l'en-tête `.page-head` / `.page-title` commun ; titre Aujourd'hui ramené à l'échelle xl.
|
||||
- **#63 Navigation** : recherche en double supprimée de l'en-tête (palette Ctrl+K + recherche sidebar restent).
|
||||
- **#64 Mobile** : cibles 44px sous 640px, en-tête et KPI compacts.
|
||||
- **#65 États** : `.empty-state` / `.focus-empty` unifiés, classe `.skeleton` (pulsation, respect de `prefers-reduced-motion`).
|
||||
- **#66 Formulaires** : classes `.field-error` / `.field-error-msg` (bordure rouge + message, jamais la couleur seule).
|
||||
- **#67 Cartes** : titre clampé à 2 lignes, code discret, labels/chips en texte thème sur fond teinté `color-mix` (contraste garanti quelle que soit la couleur Gitea).
|
||||
- **#68 Thème clair** : connexion et wallboard suivent le thème mémorisé (init avant rendu, comme `_header`) et ne restent plus sombres par défaut ; variantes claires ajoutées.
|
||||
|
||||
## 0.41.0 — 2026-10-28 — Identité Kanbante : sidebar charbon, accent vert, icônes ligne, bouton thème en Administration (#54, #55)
|
||||
|
||||
- **Sidebar (#54)** : fond charbon draco (`rgba(26, 28, 26, 0.96)`) à la place du bleu-noir `rgba(13, 16, 24)` oublié par le re-thème 0.39.0 — la barre latérale a désormais la même teinte que le reste du site, en sombre comme en clair (clair déjà blanc).
|
||||
- **Accent vert (#54)** : le rose Spark cède la place au vert Kanbante — `--accent: #0d9488`, second ton `#14b8a6` (clair : `#0f766e`), anneaux, focus, dégradés et replis associés. La barre Fusion partagée n'est pas touchée (Spark rose, marques des autres apps).
|
||||
- **Icônes (#54)** : les emoji de la barre latérale sont remplacés par des SVG fins en trait (stroke currentColor), style rail Spark, pour chaque entrée de navigation ; le logo Kanbante devient des colonnes kanban blanches sur fond vert sarcelle.
|
||||
- **Bouton Dark/Light (#55)** : déplacé de l'en-tête applicatif vers la section Administration de la barre latérale (icône seule en rail replié, libellé explicite sinon). Comportement inchangé (bascule + choix mémorisé). Conséquence : seuls les administrateurs le voient, la section Administration leur étant réservée.
|
||||
|
||||
## 0.40.0 — 2026-10-28 — Navigation : section Administration + bouton Dark/Light (#51, #52)
|
||||
|
||||
- **Accordéon (#51)** : la section Administration ne fermait jamais — le JS posait `nav-closed-admin`, mais `style.css` ne définissait les règles de fermeture que pour les 4 autres sections. Les règles admin sont ajoutées (fermeture et rotation du chevron) : l'accordéon redevient exclusif.
|
||||
- **Bouton Dark/Light (#52)** : bouton « ☀️ Mode clair / 🌙 Mode sombre » dans l'en-tête applicatif, qui bascule `data-theme` sur `<html>` et mémorise le choix (localStorage, appliqué avant le rendu par l'amorce de `_header.html`). Thème clair sur les jetons draco (fond #f2f2f0, surfaces blanches/grises, accent rose conservé, second ton assombri pour les liens), barre Fusion et panneau Spark qui suivent. Défaut inchangé : sombre. `nav.js` est désormais versionné (`?v=`), comme dashboard.js depuis la 0.38.1.
|
||||
|
||||
## 0.39.0 — 2026-10-28 — Thème du site : look agent-dev / barre Fusion (#49)
|
||||
|
||||
- **Re-thème complet** : `style.css` passe sur les jetons draco-barre (Console v7.2) — fond #101211, surfaces #1a1c1a / #242624 / #20221f, bordure #2e302c, textes #eceee9 / #a6aba2 / #8b9089, accent rose Spark #c2437e (second ton #e078a8). Fond plat, plus de halos ni de dégradés violet→bleu, y compris les valeurs de repli et l'écran de connexion.
|
||||
- **Inchangé** : couleurs sémantiques (vert/jaune/rouge) et couleurs de colonnes choisies par l'utilisateur. Pas de mode clair : l'app force le sombre, comme la console de référence.
|
||||
|
||||
## 0.38.1 — 2026-10-28 — Page Projets : le correctif 0.38.0 atteint enfin le navigateur (#47)
|
||||
|
||||
- **Cache-busting (#47)** : `dashboard.html` charge désormais `style.css?v={{ version }}` et `dashboard.js?v={{ version }}`, comme les autres pages (admin, board, project) et la barre Fusion. Sans ça, le navigateur gardait l'ancien CSS (menu transparent, bouton de copie non stylé) et l'ancien JS (aucun gestionnaire de copie) alors que le HTML était déjà neuf — c'est pourquoi la 0.38.0 semblait ne rien changer, bouton présent mais inerte. Le code 0.38.0 lui-même (jetons `--menu-bg` opaques, gestionnaire `data-copy-url`) était correct dans le dépôt.
|
||||
|
||||
## 0.38.0 — 2026-10-28 — Page Projets : copie d'URL et refonte du menu ⋯
|
||||
|
||||
- **Copie d'URL (#44)** : chaque tuile projet (grille, y compris Épinglés) propose désormais « 🔗 Copier l'URL du projet » dans son menu ⋯ — l'URL Gitea du dépôt est copiée dans le presse-papiers, avec confirmation « URL du projet copiée » et repli (textarea + execCommand) quand l'API Clipboard n'est pas disponible.
|
||||
- **Menu ⋯ (#45)** : refonte sur les jetons de la barre d'apps commune (draco-barre.css, prototype Console v7.2 / console Spark) — fond opaque, bordure, rayon 14 px, ombre portée, survol/focus visibles, séparateur avant l'action de copie. Corrige la cause du menu transparent : il utilisait `var(--surface-1)`, variable jamais définie.
|
||||
- **Thèmes sombre et clair** : jetons clairs par défaut, variante sombre via `[data-theme="dark"]` sur `<html>` et repli `prefers-color-scheme`, comme la barre Fusion.
|
||||
|
||||
## 0.37.6 — 2026-10-28 — Corrections console/recherche : CORS console Spark
|
||||
|
||||
- **CORS recherche (PR #43)** : la recherche Kanbante autorise désormais la console Spark (agent prod+dev) en inter-origines — les pages console peuvent appeler la recherche de l'hôte sans erreur CORS/réseau.
|
||||
|
||||
## 0.37.5 — 2026-10-28 — Alignement recherche console : onglet Console Spark sur /api/v1/recherche
|
||||
|
||||
- **Recherche (#68)** : l'onglet Console Spark de `/recherche` appelle désormais `GET /api/v1/recherche` (au lieu de `/api/search`) et reconstruit les liens profonds console (`?section=…&objet=…`, `disc=`, `activite=`) — les résultats console réels s'affichent enfin dans la page hôte.
|
||||
|
||||
## 0.37.4 — 2026-10-28 — Correctifs recherche/alertes/console : CORS sœurs prod+dev, recherche en onglets, alertes en onglets
|
||||
|
||||
- **CORS recherche/alertes (#68/#69)** : allowlist explicite des 8 origines sœurs prod+dev (`*.dracodev.net`, avec et sans `-dev`), jamais `*`, avec credentials — pour `app/recherche.py` et le middleware CORS de `GET /api/events`.
|
||||
|
||||
+1
-1
@@ -177,7 +177,7 @@ from urllib.parse import urlparse
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
APP_VERSION = "0.37.4"
|
||||
APP_VERSION = "0.42.0"
|
||||
|
||||
|
||||
def _env_bool(name: str, default: bool) -> bool:
|
||||
|
||||
+11
-2
@@ -10,7 +10,10 @@ testables sans base ni serveur.
|
||||
tous projets du workspace) et la regroupe par dépôt.
|
||||
- La CORS bornée n'autorise que ``GET /api/search``, et seulement
|
||||
pour les origines sœurs officielles **prod ET dev, listées
|
||||
explicitement** (``PROD_ORIGINS | DEV_ORIGINS``), **plus** les
|
||||
explicitement** (``PROD_ORIGINS | DEV_ORIGINS``), **plus les
|
||||
origines de la console Spark** (``AGENT_ORIGINS`` : agent prod ET
|
||||
dev — fédération de la recherche depuis la console, 2026-10-28),
|
||||
**plus** les
|
||||
origines résolues par ``app.config`` — jamais ``*``. Correctif
|
||||
2026-10-28 (fusion #68/#69, matrice constatée par Bruno) :
|
||||
l'ancienne allowlist ne contenait que l'environnement résolu
|
||||
@@ -44,6 +47,12 @@ DEV_ORIGINS = frozenset({
|
||||
"https://kanbante-dev.dracodev.net",
|
||||
})
|
||||
|
||||
AGENT_ORIGINS = frozenset({
|
||||
"https://agent.dracodev.net",
|
||||
"https://agent-dev.dracodev.net",
|
||||
})
|
||||
|
||||
|
||||
# Clés du dictionnaire rendu par render() (app/main.py).
|
||||
_SOEUR_KEYS = ("obsigate", "flowdeck", "tiktak", "kanbante")
|
||||
|
||||
@@ -105,7 +114,7 @@ def sister_origins(
|
||||
# Correctif 2026-10-28 : prod ET dev toujours, quelle que soit
|
||||
# la résolution — ``resolved`` n'ajoute que des origines de
|
||||
# configuration sans jamais retirer une origine officielle.
|
||||
return frozenset(origins) | PROD_ORIGINS | DEV_ORIGINS
|
||||
return frozenset(origins) | PROD_ORIGINS | DEV_ORIGINS | AGENT_ORIGINS
|
||||
|
||||
|
||||
def cors_headers_for(
|
||||
|
||||
@@ -13,14 +13,14 @@
|
||||
{% if issue.labels %}
|
||||
<div class="card-labels">
|
||||
{% for label in issue.labels %}
|
||||
<span class="label-tag" style="background: #{{ label.color }}30; color: #{{ label.color }}">
|
||||
<span class="label-tag" style="background: color-mix(in srgb, #{{ label.color }} 25%, transparent); color: var(--text)">
|
||||
{{ label.name }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if issue._code %}
|
||||
<div class="card-code">⑂{% if issue._code.branches %} 🌿 {{ issue._code.branches }}{% endif %}{% if issue._code.commits %} · {{ issue._code.commits }} commit{{ 's' if issue._code.commits > 1 }}{% endif %}{% if issue._code.pr %} · PR #{{ issue._code.pr.number }} {{ issue._code.pr.label }}{% endif %}</div>
|
||||
<div class="card-code">⑂{% if issue._code.branches %} {{ issue._code.branches }}{% endif %}{% if issue._code.commits %} · {{ issue._code.commits }} commit{{ 's' if issue._code.commits > 1 }}{% endif %}{% if issue._code.pr %} · PR #{{ issue._code.pr.number }} {{ issue._code.pr.label }}{% endif %}</div>
|
||||
{% endif %}
|
||||
<div class="card-meta">
|
||||
{% if issue.assignee %}
|
||||
@@ -30,8 +30,8 @@
|
||||
{% set age = ages.get(issue.number) %}
|
||||
{% if age and age.days >= aging_days %}
|
||||
<span class="age-badge{% if age.days >= aging_days * 2 %} stale{% endif %}"
|
||||
title="Dans cette colonne depuis le {{ age.since[:10] }}">⏳ {{ age.days }} j</span>
|
||||
title="Dans cette colonne depuis le {{ age.since[:10] }}">{{ age.days }} j</span>
|
||||
{% endif %}
|
||||
<button type="button" class="pin-btn" title="Épingler au Focus du jour (/today)">📌</button>
|
||||
<button type="button" class="pin-btn btn-icon" title="Épingler au Focus du jour (/today)" aria-label="Épingler au Focus du jour"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 4h6l-1 8 3 3H7l3-3z"/><path d="M12 15v5"/><path d="M9 20h6"/></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
<script>
|
||||
/* Thème (#52) : appliqué avant le rendu de la barre, pour
|
||||
éviter le flash sombre quand l'utilisateur a choisi le
|
||||
clair. Défaut = sombre (les gabarits portent
|
||||
data-theme="dark") ; seul un choix mémorisé bascule. */
|
||||
try {
|
||||
var _t = localStorage.getItem('kanbante-theme');
|
||||
if (_t === 'light' || _t === 'dark') {
|
||||
document.documentElement.setAttribute('data-theme', _t);
|
||||
}
|
||||
} catch (e) { /* mode privé : le thème ne survit qu'à la page */ }
|
||||
</script>
|
||||
{% include "_draco_bar.html" %}
|
||||
{# ── Cadre applicatif partagé (issue #16, refonte 0.16.0) ─────────
|
||||
Barre latérale unique pour TOUTES les pages applicatives :
|
||||
@@ -27,7 +39,7 @@
|
||||
("activity", "/activity", "📰", "Activité"),
|
||||
]),
|
||||
("outils", "Outils", [
|
||||
("search", "/search", "🔍", "Recherche"),
|
||||
("search", "/search", "", "Recherche"),
|
||||
("capture", "/capture", "⚡", "Capture"),
|
||||
("archives", "/archives", "🗄️", "Archives"),
|
||||
("wall", "/wall", "🖥️", "Wallboard"),
|
||||
@@ -78,10 +90,10 @@
|
||||
<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">📋</span>
|
||||
<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="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)">🔍</button>
|
||||
<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>
|
||||
<button type="button" class="side-collapse" id="side-collapse" title="Replier ou déplier le menu" aria-label="Replier ou déplier le menu"><span class="side-collapse-ico">«</span></button>
|
||||
</div>
|
||||
{% if current_workspace and user_workspaces and user_workspaces|length > 1 %}
|
||||
@@ -95,6 +107,9 @@
|
||||
</select>
|
||||
</form>
|
||||
{% endif %}
|
||||
{# Icônes de navigation (#54) : SVG fins en trait, style rail
|
||||
Spark — monochromes (currentColor), pas d'emoji. #}
|
||||
{% macro nav_icon(key) %}<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">{% if key == 'home' %}<path d="M3 11l9-8 9 8"/><path d="M5 10v11h14V10"/><path d="M10 21v-6h4v6"/>{% elif key == 'focus' %}<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1" fill="currentColor"/>{% elif key == 'today' %}<path d="M7 18a5 5 0 0 1 10 0"/><path d="M3 18h18"/><path d="M12 9V6"/><path d="M5.5 11l1.5 1.5"/><path d="M18.5 11l-1.5 1.5"/>{% elif key == 'my' %}<circle cx="12" cy="8" r="3.5"/><path d="M4 20c0-3.5 3.5-5.5 8-5.5s8 2 8 5.5"/>{% elif key == 'briefing' %}<path d="M4 20V11"/><path d="M10 20V4"/><path d="M16 20v-8"/><path d="M3 20h18"/>{% elif key == 'standup' %}<path d="M4 5h16v11h-7l-4 4v-4H4z"/>{% elif key == 'digest' %}<path d="M6 3h12v18H6z"/><path d="M9 8h6"/><path d="M9 12h6"/><path d="M9 16h4"/>{% elif key == 'inflight' %}<path d="M3 12l19-9-7 18-4-6z"/><path d="M11 15L22 3"/>{% elif key == 'forecast' %}<path d="M3 18l5-6 4 3 9-9"/><path d="M16 6h5v5"/>{% elif key == 'health' %}<path d="M12 20C7 16 3 13 3 9a4.5 4.5 0 0 1 8-2.5A4.5 4.5 0 0 1 21 9c0 4-4 7-9 11z"/>{% elif key == 'rules' %}<path d="M6 3h12v18H6z"/><path d="M9 12l2 2 4-4"/>{% elif key == 'activity' %}<path d="M4 4h16v16H4z"/><path d="M7 8h10"/><path d="M7 12h10"/><path d="M7 16h6"/>{% elif key == 'search' %}<circle cx="11" cy="11" r="7"/><path d="M20 20l-4-4"/>{% elif key == 'capture' %}<path d="M13 2L4 14h6l-1 8 10-13h-6z"/>{% elif key == 'archives' %}<path d="M3 8h18"/><path d="M5 8v12h14V8"/><path d="M9 8v5h6V8"/>{% elif key == 'wall' %}<path d="M3 5h18v11H3z"/><path d="M9 21h6"/><path d="M12 16v5"/>{% elif key == 'admin' %}<path d="M4 4h7v7H4z"/><path d="M13 4h7v7h-7z"/><path d="M4 13h7v7H4z"/><path d="M13 13h7v7h-7z"/>{% elif key == 'admin_connections' %}<path d="M9 3v5"/><path d="M15 3v5"/><path d="M7 8h10v5a5 5 0 0 1-10 0z"/><path d="M12 18v3"/>{% elif key == 'admin_workspaces' %}<circle cx="12" cy="12" r="9"/><path d="M15 9l-2 4-4 2 2-4z"/>{% elif key == 'admin_columns' %}<path d="M4 4h4v16H4z"/><path d="M10 4h4v16h-4z"/><path d="M16 4h4v16h-4z"/>{% elif key == 'admin_settings' %}<circle cx="12" cy="12" r="3"/><path d="M12 2v3"/><path d="M12 19v3"/><path d="M2 12h3"/><path d="M19 12h3"/><path d="M4.5 4.5l2 2"/><path d="M17.5 17.5l2 2"/><path d="M19.5 4.5l-2 2"/><path d="M6.5 17.5l-2 2"/>{% else %}<circle cx="12" cy="12" r="9"/>{% endif %}</svg>{% endmacro %}
|
||||
<nav class="side-nav">
|
||||
{% for gkey, group, links in nav_groups %}
|
||||
<div class="side-section" data-group="{{ gkey }}">
|
||||
@@ -106,10 +121,16 @@
|
||||
<div class="side-acc-inner">
|
||||
{% for key, href, icon, label in links %}
|
||||
<a href="{{ href }}" class="side-link{% if active_nav == key %} active{% endif %}" title="{{ label }}">
|
||||
<span class="side-ico">{{ icon }}</span>
|
||||
<span class="side-ico">{{ nav_icon(key) }}</span>
|
||||
<span class="side-label">{{ label }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% if gkey == 'admin' %}
|
||||
<button type="button" class="side-link theme-toggle-link" id="theme-toggle" title="Basculer entre thème sombre et clair" aria-label="Basculer entre thème sombre et clair">
|
||||
<span class="side-ico" data-theme-ico>☀️</span>
|
||||
<span class="side-label" data-theme-label>Mode clair</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -123,6 +144,9 @@
|
||||
<a href="/profile" class="side-logout" title="Mon profil">👤<span class="side-label"> {{ current_user.display_name or current_user.username }}</span></a>
|
||||
{% endif %}
|
||||
<span class="side-version side-label">v{{ version }}</span>
|
||||
{% if not is_admin %}
|
||||
<button type="button" class="theme-toggle-foot" id="theme-toggle" title="Basculer entre thème sombre et clair" aria-label="Basculer entre thème sombre et clair"><span data-theme-ico>☀️</span></button>
|
||||
{% endif %}
|
||||
{% if auth_enabled %}
|
||||
<a href="/logout" class="side-logout" title="Se déconnecter">⏻<span class="side-label"> Sortir</span></a>
|
||||
{% endif %}
|
||||
@@ -140,7 +164,6 @@
|
||||
{% if repo_info is defined and repo_info and repo_info.html_url %}
|
||||
<a href="{{ repo_info.html_url }}" target="_blank" rel="noopener" class="nav-pill" title="Ouvrir ce dépôt dans Gitea">🔗 Gitea ↗</a>
|
||||
{% endif %}
|
||||
<button type="button" class="nav-pill palette-trigger topbar-search" title="Recherche et commandes (Ctrl+K)">🔍</button>
|
||||
</div>
|
||||
<script src="/static/js/nav.js" defer></script>
|
||||
<script src="/static/js/nav.js?v={{ version }}" defer></script>
|
||||
</header>
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
|
||||
<main class="focus-main">
|
||||
<div class="page-head">
|
||||
<h1>🛠️ Administration</h1>
|
||||
<h1>Administration</h1>
|
||||
<p class="page-sub">Utilisateurs, vue d'ensemble et journal d'administration.</p>
|
||||
</div>
|
||||
{% if msg %}<p class="chip-ok">✅ {{ msg }}</p>{% endif %}
|
||||
{% if err %}<p class="chip-warn">⚠️ {{ err }}</p>{% endif %}
|
||||
{% if err %}<p class="chip-warn">{{ err }}</p>{% endif %}
|
||||
|
||||
<section class="briefing-card">
|
||||
<h2>👥 Utilisateurs ({{ users|length }})</h2>
|
||||
@@ -79,7 +79,7 @@
|
||||
<a href="/admin/connections">🔌 Connexions ({{ connections|length }})</a> ·
|
||||
<a href="/admin/workspaces">🧭 Espaces ({{ workspaces|length }})</a> ·
|
||||
<a href="/admin/settings">🎛️ Paramètres</a> ·
|
||||
<a href="/archives">🗄️ Archives</a>
|
||||
<a href="/archives">Archives</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
|
||||
<main class="focus-main">
|
||||
<div class="page-head">
|
||||
<h1>🧱 Colonnes du board</h1>
|
||||
<h1>Colonnes du board</h1>
|
||||
<p class="page-sub">Espace : <strong>{{ workspace.name }}</strong> (change d'espace par le sélecteur en haut pour régler un autre board). Les colonnes sont propres à chaque espace : nom, couleur, ordre et <strong>mapping étiquette ↔ colonne</strong>. La <strong>clé</strong> d'une colonne ne change jamais (les cartes y sont rattachées) ; le <strong>nom affiché</strong>, lui, est libre.</p>
|
||||
</div>
|
||||
{% if msg %}<p class="chip-ok">✅ {{ msg }}</p>{% endif %}
|
||||
{% if err %}<p class="chip-warn">⚠️ {{ err }}</p>{% endif %}
|
||||
{% if err %}<p class="chip-warn">{{ err }}</p>{% endif %}
|
||||
|
||||
<section class="briefing-card">
|
||||
<h2>Comment marche le mapping</h2>
|
||||
@@ -52,12 +52,12 @@
|
||||
<form method="post" action="/admin/columns/{{ c.key }}/move" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="direction" value="up">
|
||||
<button type="submit" class="btn-mini" title="Monter">⬆️</button>
|
||||
<button type="submit" class="btn-mini" title="Monter">↑</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/columns/{{ c.key }}/move" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="direction" value="down">
|
||||
<button type="submit" class="btn-mini" title="Descendre">⬇️</button>
|
||||
<button type="submit" class="btn-mini" title="Descendre">↓</button>
|
||||
</form>
|
||||
{% if c.kind == 'flow' %}
|
||||
<form method="post" action="/admin/columns/{{ c.key }}/delete" class="inline-form" onsubmit="return confirm('Supprimer cette colonne ? Ses cartes iront dans la colonne choisie.');">
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
|
||||
<main class="focus-main">
|
||||
<div class="page-head">
|
||||
<h1>🔌 Connexions aux forges</h1>
|
||||
<h1>Connexions aux forges</h1>
|
||||
<p class="page-sub">Chaque connexion est un accès Gitea ou GitHub. Les jetons sont <strong>chiffrés au repos</strong> et ne sont <strong>jamais réaffichés</strong> — pour en changer un, saisis la nouvelle valeur.</p>
|
||||
</div>
|
||||
{% if msg %}<p class="chip-ok">✅ {{ msg }}</p>{% endif %}
|
||||
{% if err %}<p class="chip-warn">⚠️ {{ err }}</p>{% endif %}
|
||||
{% if err %}<p class="chip-warn">{{ err }}</p>{% endif %}
|
||||
|
||||
{% for c in connections %}
|
||||
<section class="briefing-card">
|
||||
@@ -41,7 +41,7 @@
|
||||
<p class="admin-actions">
|
||||
<form method="post" action="/admin/connections/{{ c.id }}/test" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" class="btn-mini">🧪 Tester la connexion</button>
|
||||
<button type="submit" class="btn-mini">Tester la connexion</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/connections/{{ c.id }}/toggle" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
|
||||
<main class="focus-main">
|
||||
<div class="page-head">
|
||||
<h1>🎛️ Paramètres</h1>
|
||||
<h1>Paramètres</h1>
|
||||
<p class="page-sub"><strong>L'environnement amorce, le panneau gouverne.</strong> Une valeur saisie ici remplace celle de l'environnement (sans redémarrage pour la plupart) ; un champ vidé revient à la valeur d'environnement. Les secrets et les réglages d'amorçage (base de données, clé maître, session, draco) restent en environnement, volontairement.</p>
|
||||
</div>
|
||||
{% if msg %}<p class="chip-ok">✅ {{ msg }}</p>{% endif %}
|
||||
{% if err %}<p class="chip-warn">⚠️ {{ err }}</p>{% endif %}
|
||||
{% if err %}<p class="chip-warn">{{ err }}</p>{% endif %}
|
||||
|
||||
<section class="briefing-card">
|
||||
<form method="post" action="/admin/settings">
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
|
||||
<main class="focus-main">
|
||||
<div class="page-head">
|
||||
<h1>🧭 Espaces de travail</h1>
|
||||
<h1>Espaces de travail</h1>
|
||||
<p class="page-sub">Un espace regroupe des connexions (forges) et des membres. Le board d'un projet appartient à l'espace : deux espaces peuvent ranger le même projet différemment. L'état de board, les notes et les épingles du jour sont propres à chaque espace.</p>
|
||||
</div>
|
||||
{% if msg %}<p class="chip-ok">✅ {{ msg }}</p>{% endif %}
|
||||
{% if err %}<p class="chip-warn">⚠️ {{ err }}</p>{% endif %}
|
||||
{% if err %}<p class="chip-warn">{{ err }}</p>{% endif %}
|
||||
|
||||
{% for d in details %}
|
||||
{% set ws = d.ws %}
|
||||
|
||||
+12
-12
@@ -12,11 +12,11 @@
|
||||
|
||||
<main class="focus-main">
|
||||
<div class="page-head">
|
||||
<h1>🗄️ Archives</h1>
|
||||
<h1>Archives</h1>
|
||||
<p class="page-sub">Espace : <strong>{{ workspace.name if workspace else '—' }}</strong>. Deux sortes d'archives : <strong>locale</strong> (le projet disparaît des vues de cet espace, la forge n'est pas touchée, réversible en un clic) et <strong>sur la forge</strong> (propriété du dépôt, lecture seule là-bas). Les archives sortent des rituels et des instantanés, mais restent cherchables.</p>
|
||||
</div>
|
||||
{% if msg %}<p class="chip-ok">✅ {{ msg }}</p>{% endif %}
|
||||
{% if err %}<p class="chip-warn">⚠️ {{ err }}</p>{% endif %}
|
||||
{% if err %}<p class="chip-warn">{{ err }}</p>{% endif %}
|
||||
|
||||
{% macro actions(row, kind) %}
|
||||
{% if can_write %}
|
||||
@@ -26,7 +26,7 @@
|
||||
<input type="hidden" name="conn_id" value="{{ row.conn_id }}">
|
||||
<input type="hidden" name="repo_full_name" value="{{ row.full_name }}">
|
||||
<input type="hidden" name="archived" value="1">
|
||||
<button type="submit" class="btn-mini">📥 Archiver ici</button>
|
||||
<button type="submit" class="btn-mini">Archiver ici</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if kind == 'local' %}
|
||||
@@ -35,7 +35,7 @@
|
||||
<input type="hidden" name="conn_id" value="{{ row.conn_id }}">
|
||||
<input type="hidden" name="repo_full_name" value="{{ row.full_name }}">
|
||||
<input type="hidden" name="archived" value="0">
|
||||
<button type="submit" class="btn-mini">♻️ Restaurer</button>
|
||||
<button type="submit" class="btn-mini">Restaurer</button>
|
||||
</form>
|
||||
<form method="post" action="/archives/purge" class="inline-form" onsubmit="return confirm('Purger l\'état local (board, notes, épingles) de ce projet dans cet espace ? La forge n\'est pas touchée.');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
@@ -50,7 +50,7 @@
|
||||
<input type="hidden" name="conn_id" value="{{ row.conn_id }}">
|
||||
<input type="hidden" name="repo_full_name" value="{{ row.full_name }}">
|
||||
<input type="hidden" name="archived" value="1">
|
||||
<button type="submit" class="btn-mini">🔒 Archiver sur la forge</button>
|
||||
<button type="submit" class="btn-mini">Archiver sur la forge</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if kind == 'forge' %}
|
||||
@@ -59,14 +59,14 @@
|
||||
<input type="hidden" name="conn_id" value="{{ row.conn_id }}">
|
||||
<input type="hidden" name="repo_full_name" value="{{ row.full_name }}">
|
||||
<input type="hidden" name="archived" value="0">
|
||||
<button type="submit" class="btn-mini">🔓 Désarchiver sur la forge</button>
|
||||
<button type="submit" class="btn-mini">Désarchiver sur la forge</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="post" action="/archives/export" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="conn_id" value="{{ row.conn_id }}">
|
||||
<input type="hidden" name="repo_full_name" value="{{ row.full_name }}">
|
||||
<button type="submit" class="btn-mini">⬇️ Récap (.md)</button>
|
||||
<button type="submit" class="btn-mini">Récap (.md)</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
@@ -80,7 +80,7 @@
|
||||
{% if row.missing %}<span class="chip-warn">introuvable sur la forge</span>{% endif %}
|
||||
</td>
|
||||
<td>{{ row.updated_at or '—' }}{% if row.idle_days is not none %} <span class="muted">(il y a {{ row.idle_days }} j)</span>{% endif %}</td>
|
||||
<td>{% if row.open_issues %}⚠️ {{ row.open_issues }} ouverte(s){% else %}0{% endif %}</td>
|
||||
<td>{% if row.open_issues %}{{ row.open_issues }} ouverte(s){% else %}0{% endif %}</td>
|
||||
<td class="admin-actions">{{ actions(row, kind) }}</td>
|
||||
</tr>
|
||||
{% endmacro %}
|
||||
@@ -98,13 +98,13 @@
|
||||
<td><input type="checkbox" name="item" value="{{ row.conn_id }}:{{ row.full_name }}" checked></td>
|
||||
<td>{% if row.conn_kind == 'github' %}🐙{% else %}🍵{% endif %} <strong>{{ row.full_name }}</strong> {% if row.conn_slug %}<span class="conn-badge">{{ row.conn_slug }}</span>{% endif %}</td>
|
||||
<td>{{ row.updated_at }} <span class="muted">(il y a {{ row.idle_days }} j)</span></td>
|
||||
<td>{% if row.open_issues %}⚠️ {{ row.open_issues }} ouverte(s){% else %}0{% endif %}</td>
|
||||
<td>{% if row.open_issues %}{{ row.open_issues }} ouverte(s){% else %}0{% endif %}</td>
|
||||
<td class="admin-actions">{{ actions(row, 'suggested') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p><button type="submit" class="btn-save btn-inline">📥 Archiver localement la sélection</button></p>
|
||||
<p><button type="submit" class="btn-save btn-inline">Archiver localement la sélection</button></p>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="muted">Rien à proposer : aucun projet inactif au-delà du seuil (réglable : <code>archive_suggest_days</code> dans Administration → Paramètres).</p>
|
||||
@@ -112,7 +112,7 @@
|
||||
</section>
|
||||
|
||||
<section class="briefing-card">
|
||||
<h2>📥 Archives locales de cet espace ({{ groups.local|length }})</h2>
|
||||
<h2>Archives locales de cet espace ({{ groups.local|length }})</h2>
|
||||
{% if groups.local %}
|
||||
<table class="proj-table">
|
||||
<thead><tr><th>Projet</th><th>Dernière activité</th><th>Issues ouvertes</th><th>Actions</th></tr></thead>
|
||||
@@ -124,7 +124,7 @@
|
||||
</section>
|
||||
|
||||
<section class="briefing-card">
|
||||
<h2>🔒 Archivés sur la forge ({{ groups.forge|length }})</h2>
|
||||
<h2>Archivés sur la forge ({{ groups.forge|length }})</h2>
|
||||
{% if groups.forge %}
|
||||
<table class="proj-table">
|
||||
<thead><tr><th>Projet</th><th>Dernière activité</th><th>Issues ouvertes</th><th>Actions</th></tr></thead>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
<!-- Toolbar: text + label filters, and issue creation (Gitea-backed) -->
|
||||
<div class="board-toolbar">
|
||||
<input type="text" id="card-filter" placeholder="🔍 Filtrer les cartes... ( / )">
|
||||
<input type="text" id="card-filter" placeholder="Filtrer les cartes... ( / )">
|
||||
<select id="label-filter">
|
||||
<option value="">Toutes les étiquettes</option>
|
||||
{% for label in labels %}
|
||||
@@ -52,7 +52,7 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="button" id="labels-btn" class="btn-sm">🏷️ Gérer les étiquettes</button>
|
||||
<a href="/triage/{{ full_name }}" class="btn-sm" style="text-decoration:none">📥 Triage du backlog</a>
|
||||
<a href="/triage/{{ full_name }}" class="btn-sm" style="text-decoration:none">Triage du backlog</a>
|
||||
{% if is_admin or workspace_role == 'admin' %}<a href="/admin/columns" class="btn-sm" style="text-decoration:none" title="Personnaliser les colonnes de cet espace">🧱 Colonnes</a>{% endif %}
|
||||
<form id="create-issue-form" class="composer">
|
||||
<div class="composer-head">
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<main class="focus-main">
|
||||
<section class="briefing-card">
|
||||
<h1>⚡ Capture éclair</h1>
|
||||
<h1>Capture éclair</h1>
|
||||
<p>Une idée, un bogue, une pensée — capturés en deux secondes, ils deviennent une issue dans le bon backlog.</p>
|
||||
</section>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<input type="text" name="repo_full_name" id="capture-repo" value="{{ capture_repo }}" placeholder="Projets/kanbante" required autocomplete="off" role="combobox" aria-expanded="false" aria-controls="repo-listbox" aria-autocomplete="list">
|
||||
<span class="repo-list" id="repo-listbox" role="listbox" hidden></span>
|
||||
</span>
|
||||
<span class="repo-hint" id="repo-hint" hidden>⚠️ Aucun dépôt de ce nom dans Gitea — vérifie l'orthographe, la capture échouera sinon.</span>
|
||||
<span class="repo-hint" id="repo-hint" hidden>Aucun dépôt de ce nom dans Gitea — vérifie l'orthographe, la capture échouera sinon.</span>
|
||||
<span class="repo-tip">Astuce : tape un début, <kbd>Tab</kbd> complète segment par segment (<code>pr</code> → <code>Projets/</code> → <code>kan</code> → <code>kanbante</code>), ↑↓ + Entrée choisissent dans la liste.</span>
|
||||
</label>
|
||||
<label>Titre de l'issue
|
||||
@@ -52,8 +52,8 @@
|
||||
</label>
|
||||
<label>Colonne
|
||||
<select name="column">
|
||||
<option value="backlog" selected>📥 Backlog</option>
|
||||
<option value="todo">📋 À faire</option>
|
||||
<option value="backlog" selected>Backlog</option>
|
||||
<option value="todo">À faire</option>
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit" class="btn-save">⚡ Capturer</button>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Kanbante — Dashboard</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/style.css?v={{ version }}">
|
||||
<script defer src="/static/vendor/alpine.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -55,7 +55,7 @@
|
||||
<button type="button" role="tab" :class="{ active: tab === 'active' }" @click="tab = 'active'">Actifs <span class="seg-count">{{ active_repos|length }}</span></button>
|
||||
<button type="button" role="tab" :class="{ active: tab === 'inactive' }" @click="tab = 'inactive'">Inactifs <span class="seg-count">{{ inactive_repos|length }}</span></button>
|
||||
<button type="button" role="tab" :class="{ active: tab === 'archived' }" @click="tab = 'archived'">Archivés <span class="seg-count">{{ archived_repos|length }}</span></button>
|
||||
<a href="/archives" class="seg-link" title="Gérer les archives (locales et forge)">🗄️ Gérer</a>
|
||||
<a href="/archives" class="seg-link" title="Gérer les archives (locales et forge)">Gérer</a>
|
||||
</div>
|
||||
<div class="search-bar toolbar-search">
|
||||
<input type="text" id="project-search" placeholder="Filtrer les projets..." x-model="search" autocomplete="off">
|
||||
@@ -64,7 +64,7 @@
|
||||
<label class="dash-tool">Tri
|
||||
<select id="dash-sort">
|
||||
<option value="default">Par défaut</option>
|
||||
<option value="review">🔍 En attente de revue ↓</option>
|
||||
<option value="review">En attente de revue ↓</option>
|
||||
<option value="updated">Activité Gitea récente</option>
|
||||
<option value="activity">Activité kanban récente</option>
|
||||
<option value="open">Issues ouvertes ↓</option>
|
||||
@@ -102,13 +102,15 @@
|
||||
<div class="card-menu-wrap">
|
||||
<button type="button" class="card-menu-btn" data-menu-btn aria-haspopup="true" aria-expanded="false" title="Actions rapides pour {{ repo.name }}" aria-label="Actions rapides pour {{ repo.name }}">⋯</button>
|
||||
<div class="card-menu" hidden>
|
||||
<a href="/board/{{ full }}">📋 Board</a>
|
||||
<a href="/board/{{ full }}">Board</a>
|
||||
<a href="/capture?repo={{ full|urlencode }}">⚡ Capturer ici</a>
|
||||
<a href="/triage/{{ full }}">📥 Triage du backlog</a>
|
||||
<a href="/triage/{{ full }}">Triage du backlog</a>
|
||||
<a href="/health/{{ full }}">🩺 Santé</a>
|
||||
<a href="/sprint/{{ full }}">📖 Récit de sprint</a>
|
||||
{% if repo.html_url %}
|
||||
<a href="{{ repo.html_url }}" target="_blank" rel="noopener">🔗 Gitea ↗</a>
|
||||
<div class="card-menu-sep" role="separator"></div>
|
||||
<button type="button" class="card-menu-item" data-copy-url="{{ repo.html_url }}" title="Copier l'URL Gitea de {{ repo.name }}" aria-label="Copier l'URL du projet {{ repo.name }}">🔗 Copier l'URL du projet</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,17 +134,17 @@
|
||||
<div class="ms-block">
|
||||
<div class="ms-line">
|
||||
<a href="/forecast#ms-{{ repo.owner.login }}-{{ repo.name }}" class="ms-title" title="Voir ce jalon dans le tableau des prévisions">🎯 {{ ms.title }}</a>
|
||||
{% if ms.due_label %}<span class="ms-due{% if ms.late %} late{% endif %}">{{ '⚠️ ' if ms.late }}échéance {{ ms.due_label }}</span>{% endif %}
|
||||
{% if ms.due_label %}<span class="ms-due{% if ms.late %} late{% endif %}">{{ '' if ms.late }}échéance {{ ms.due_label }}</span>{% endif %}
|
||||
</div>
|
||||
{% if ms.total %}
|
||||
<div class="progress ms-progress"><span style="width: {{ ms.pct }}%"></span></div>
|
||||
<div class="ms-meta">{{ ms.closed }}/{{ ms.total }} terminées{% if ms.eta_label %} · 🔮 {{ ms.eta_label }}{% if ms.eta_late %} — ⚠️ après l'échéance{% endif %}{% endif %}</div>
|
||||
<div class="ms-meta">{{ ms.closed }}/{{ ms.total }} terminées{% if ms.eta_label %} · 🔮 {{ ms.eta_label }}{% if ms.eta_late %} — après l'échéance{% endif %}{% endif %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card-footer">
|
||||
<span>{{ repo.open_issues_count or 0 }} issues ouvertes</span>
|
||||
<a href="/board/{{ full }}" class="card-board-link">📋 Board</a>
|
||||
<a href="/board/{{ full }}" class="card-board-link">Board</a>
|
||||
{% if repo.forecast_label %}
|
||||
<span class="forecast-line" title="Prévision d'après le débit réel du journal (fermetures par semaine)">🔮 {{ repo.forecast_label }}</span>
|
||||
{% endif %}
|
||||
@@ -151,7 +153,7 @@
|
||||
{% for col in column_keys %}
|
||||
{% set n = stats[full].get(col, 0) %}
|
||||
{% if n > 0 %}
|
||||
<span class="chip" style="background: {{ COLUMN_COLORS[col] }}20; color: {{ COLUMN_COLORS[col] }}">
|
||||
<span class="chip" style="background: color-mix(in srgb, {{ COLUMN_COLORS[col] }} 25%, transparent); color: var(--text)">
|
||||
{{ n }}
|
||||
</span>
|
||||
{% endif %}
|
||||
@@ -184,7 +186,7 @@
|
||||
<td>{{ repo.owner.login }}</td>
|
||||
<td>{{ repo.open_issues_count or 0 }}</td>
|
||||
<td>{{ st.get('in_progress', 0) + st.get('qa', 0) + st.get('review', 0) }}</td>
|
||||
<td>{% if repo.next_milestone %}🎯 {{ repo.next_milestone.title }}{% if repo.next_milestone.late %} ⚠️{% endif %}{% else %}—{% endif %}</td>
|
||||
<td>{% if repo.next_milestone %}🎯 {{ repo.next_milestone.title }}{% if repo.next_milestone.late %} {% endif %}{% else %}—{% endif %}</td>
|
||||
<td>{% if repo.health_dot %}<a href="/health/{{ full }}" class="health-dot {{ repo.health_dot.level }}" title="{{ repo.health_dot.tooltip }}" aria-label="Santé : {{ repo.health_dot.tooltip }}"></a>{% else %}—{% endif %}</td>
|
||||
<td>{{ repo.updated_at[:10] }}</td>
|
||||
</tr>
|
||||
@@ -226,21 +228,21 @@
|
||||
<div x-show="tab === 'active'" class="project-grid" id="grid-active">
|
||||
{% for repo in active_listed %}{{ project_card(repo) }}{% endfor %}
|
||||
{% if not active_listed and not pinned_repos %}
|
||||
<div class="empty-state">🌱 Aucun projet actif en ce moment. Les projets bougent dès qu'une issue ou un commit arrive dans Gitea.</div>
|
||||
<div class="empty-state">Aucun projet actif en ce moment. Les projets bougent dès qu'une issue ou un commit arrive dans Gitea.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div x-show="tab === 'inactive'" class="project-grid" id="grid-inactive">
|
||||
{% for repo in inactive_listed %}{{ project_card(repo) }}{% endfor %}
|
||||
{% if not inactive_listed %}
|
||||
<div class="empty-state">✨ Rien d'endormi : tous vos projets ont eu de l'activité récemment.</div>
|
||||
<div class="empty-state">Rien d'endormi : tous vos projets ont eu de l'activité récemment.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div x-show="tab === 'archived'" class="project-grid" id="grid-archived">
|
||||
{% for repo in archived_repos %}{{ project_card(repo, archived=True) }}{% endfor %}
|
||||
{% if not archived_repos %}
|
||||
<div class="empty-state">🗄️ Aucun projet archivé. L'archivage se fait dans Gitea (Réglages du dépôt).</div>
|
||||
<div class="empty-state">Aucun projet archivé. L'archivage se fait dans Gitea (Réglages du dépôt).</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -259,7 +261,7 @@
|
||||
<script src="/static/js/live.js" defer></script>
|
||||
<script>window.KANBANTE_CSRF = "{{ csrf_token }}";</script>
|
||||
<script src="/static/js/stars.js" defer></script>
|
||||
<script src="/static/js/dashboard.js" defer></script>
|
||||
<script src="/static/js/dashboard.js?v={{ version }}" defer></script>
|
||||
<script src="/static/js/tablesort.js" defer></script>
|
||||
<script>
|
||||
document.addEventListener('keydown', function (e) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<main class="focus-main">
|
||||
<section class="briefing-card">
|
||||
<h1>🧠 Digest hebdo → ObsiGate</h1>
|
||||
<h1>Digest hebdo → ObsiGate</h1>
|
||||
<p>Chaque semaine, Kanbante compose le résumé de l'activité (livré, en cours, chiffres du journal) en une note markdown.
|
||||
L'envoi vers ObsiGate est une écriture dans un autre système : <strong>il n'a jamais lieu sans ton approbation ici</strong>.</p>
|
||||
{% if last_result %}<p>🧾 Dernier envoi : <code>{{ last_result }}</code></p>{% endif %}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
{% include "_header.html" %}
|
||||
<main class="error-main">
|
||||
<div class="error-box">
|
||||
<h2>⚠️ Quelque chose s'est mal passé</h2>
|
||||
<h2>Quelque chose s'est mal passé</h2>
|
||||
<p>{{ message }}</p>
|
||||
<p><a href="/" class="btn-sm">← Retour au dashboard</a></p>
|
||||
</div>
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
{% include "_header.html" %}
|
||||
|
||||
<main class="focus-main">
|
||||
<p class="focus-intro">Tout ce qui est <strong>En cours</strong> ou <strong>En révision</strong>, tous projets actifs confondus.</p>
|
||||
<div class="page-head"><div class="page-title"><h1>Focus</h1><p class="page-sub">Tout ce qui est <strong>En cours</strong> ou <strong>En révision</strong>, tous projets actifs confondus.</p></div></div>
|
||||
{% if not groups %}
|
||||
<p class="focus-empty">🎉 Rien en cours ni en révision pour le moment.</p>
|
||||
<p class="focus-empty">Rien en cours ni en révision pour le moment.</p>
|
||||
{% endif %}
|
||||
{% for group in groups %}
|
||||
<section class="focus-group">
|
||||
@@ -26,7 +26,7 @@
|
||||
<div class="kanban-card focus-card">
|
||||
<div class="card-id">
|
||||
#{{ card.issue.number }}
|
||||
<span class="chip" style="background: {{ column_colors[card.column] }}20; color: {{ column_colors[card.column] }}">
|
||||
<span class="chip" style="background: color-mix(in srgb, {{ column_colors[card.column] }} 25%, transparent); color: var(--text)">
|
||||
{{ column_labels[card.column] }}
|
||||
</span>
|
||||
{% if card.issue.html_url %}
|
||||
@@ -37,7 +37,7 @@
|
||||
{% if card.issue.labels %}
|
||||
<div class="card-labels">
|
||||
{% for label in card.issue.labels %}
|
||||
<span class="label-tag" style="background: #{{ label.color }}30; color: #{{ label.color }}">
|
||||
<span class="label-tag" style="background: color-mix(in srgb, #{{ label.color }} 25%, transparent); color: var(--text)">
|
||||
{{ label.name }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
|
||||
@@ -12,15 +12,15 @@
|
||||
|
||||
<main class="focus-main">
|
||||
<section class="briefing-card">
|
||||
<h1>🔮 Prévisions — « on finit quand ? »</h1>
|
||||
<h1>Prévisions — « on finit quand ? »</h1>
|
||||
<p>Le tableau de bord des jalons, <strong>tous projets confondus</strong>, trié par risque : l'écart entre la date estimée (au débit réel du journal) et l'échéance. Réponse honnête — fourchettes plutôt que faux chiffres précis, et aucune prévision sans historique de fermetures.</p>
|
||||
<p class="forecast-summary">
|
||||
<span class="chip chip-total">🎯 {{ summary.total }} jalon(s) ouvert(s)</span>
|
||||
{% if summary.late %}<span class="chip chip-late">⚠️ {{ summary.late }} en retard à ce rythme</span>{% endif %}
|
||||
{% if summary.late %}<span class="chip chip-late">{{ summary.late }} en retard à ce rythme</span>{% endif %}
|
||||
{% if summary.ontime %}<span class="chip chip-ok">✅ {{ summary.ontime }} dans les temps</span>{% endif %}
|
||||
</p>
|
||||
<div class="search-bar forecast-filter">
|
||||
<input type="text" id="ms-filter" placeholder="🔍 Filtrer par projet ou jalon..." autocomplete="off">
|
||||
<input type="text" id="ms-filter" placeholder="Filtrer par projet ou jalon..." autocomplete="off">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
<td>{{ row.eta or '—' }}</td>
|
||||
<td>
|
||||
{% if row.verdict == 'late' %}
|
||||
<span class="verdict verdict-late">⚠️ +{{ row.gap_days }} j</span>
|
||||
<span class="verdict verdict-late">+{{ row.gap_days }} j</span>
|
||||
{% elif row.verdict == 'ontime' %}
|
||||
<span class="verdict verdict-ok">✅ dans les temps{% if row.gap_days is not none and row.gap_days < 0 %} ({{ -row.gap_days }} j d'avance){% endif %}</span>
|
||||
{% elif row.verdict == 'nodue' %}
|
||||
@@ -78,7 +78,7 @@
|
||||
<h2>📈 Débit par projet</h2>
|
||||
{% for project in projects %}
|
||||
<div class="forecast-flex">
|
||||
<div class="forecast-proj-name">📂 <a href="/project/{{ project.full_name }}">{{ project.full_name }}</a></div>
|
||||
<div class="forecast-proj-name"><a href="/project/{{ project.full_name }}">{{ project.full_name }}</a></div>
|
||||
<svg class="sparkline" viewBox="0 0 168 48" role="img" aria-label="Fermetures par semaine">
|
||||
{% for bar in project.bars %}
|
||||
{% set px = (bar.h * 0.42)|round|int %}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<main class="focus-main">
|
||||
<section class="briefing-card">
|
||||
<h1>🩺 Santé du flux</h1>
|
||||
<h1>Santé du flux</h1>
|
||||
<p>Tous les projets actifs, <strong>triés par sévérité</strong> — le rouge d'abord. Quelle colonne accumule (temps médian par colonne, d'après le journal) et quelles cartes sont devenues des <strong>zombies</strong>. Cliquez un projet pour le détail et les actions rapides.</p>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -25,12 +25,12 @@
|
||||
{% endif %}
|
||||
{% if health is none %}
|
||||
<section class="briefing-card">
|
||||
<h1>🩺 {{ full_name }}</h1>
|
||||
<h1>{{ full_name }}</h1>
|
||||
<p>Projet introuvable.</p>
|
||||
</section>
|
||||
{% else %}
|
||||
<section class="briefing-card">
|
||||
<h1>🩺 {{ full_name }}</h1>
|
||||
<h1>{{ full_name }}</h1>
|
||||
<h2>⏱️ Temps passé par colonne (journal)</h2>
|
||||
{% if health.dwell %}
|
||||
<table class="health-table">
|
||||
@@ -71,7 +71,7 @@
|
||||
</ul>
|
||||
<p class="notes-hint">« Fermer » déplace la carte en Terminé (l'issue est fermée dans Gitea par la synchro habituelle). « Toujours pertinent ? » poste un commentaire du même texte sur l'issue.</p>
|
||||
{% else %}
|
||||
<p>Aucune carte zombie. Le flux est propre. ✨</p>
|
||||
<p>Aucune carte zombie. Le flux est propre. </p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
<div class="inflight-summary">
|
||||
<span class="inflight-total">🛩️ {{ total }} carte{{ 's' if total != 1 }} en vol</span>
|
||||
{% for group in groups %}
|
||||
<span class="col-chip" style="background: {{ column_colors[group.column] }}20; color: {{ column_colors[group.column] }}">
|
||||
<span class="col-chip" style="background: color-mix(in srgb, {{ column_colors[group.column] }} 25%, transparent); color: var(--text)">
|
||||
{{ column_labels[group.column] }} · {{ group.cards|length }}{% if wip_globals.get(group.column) %} / limite globale {{ wip_globals[group.column] }}{% endif %}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if not groups %}
|
||||
<p class="focus-empty">🎉 Rien en vol pour le moment — aucun encours, aucune révision en attente.</p>
|
||||
<p class="focus-empty">Rien en vol pour le moment — aucun encours, aucune révision en attente.</p>
|
||||
{% endif %}
|
||||
|
||||
{% for group in groups %}
|
||||
@@ -51,7 +51,7 @@
|
||||
{% if card.issue.labels %}
|
||||
<div class="card-labels">
|
||||
{% for label in card.issue.labels %}
|
||||
<span class="label-tag" style="background: #{{ label.color }}30; color: #{{ label.color }}">
|
||||
<span class="label-tag" style="background: color-mix(in srgb, #{{ label.color }} 25%, transparent); color: var(--text)">
|
||||
{{ label.name }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
@@ -62,7 +62,7 @@
|
||||
<img src="{{ card.issue.assignee.avatar_url }}" class="avatar" alt="{{ card.issue.assignee.login }}">
|
||||
{% endif %}
|
||||
<span class="age-badge{% if card.age_days >= aging_days * 2 %} stale{% elif card.age_days >= aging_days %} warn{% endif %}"
|
||||
title="Dans cette colonne depuis le {{ card.age_since[:10] }}">⏳ {{ card.age_days }} j dans la colonne</span>
|
||||
title="Dans cette colonne depuis le {{ card.age_since[:10] }}">{{ card.age_days }} j dans la colonne</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr" data-theme="dark">
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script>try{var t=localStorage.getItem("kanbante-theme");document.documentElement.setAttribute("data-theme",t==="light"?"light":"dark");}catch(e){document.documentElement.setAttribute("data-theme","dark");}</script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="csrf-token" content="{{ csrf_token }}">
|
||||
<title>Connexion — Kanbante</title>
|
||||
@@ -11,12 +12,12 @@
|
||||
<main class="login-wrap">
|
||||
<section class="login-card">
|
||||
<div class="login-brand">
|
||||
<span class="login-mark" aria-hidden="true">📋</span>
|
||||
<span class="login-mark" aria-hidden="true"><svg viewBox="0 0 24 24" fill="#fff" aria-hidden="true" style="width:20px;height:20px;display:block"><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>
|
||||
<h1>Kanbante</h1>
|
||||
<p class="login-sub">Pilotage de projets, adossé à votre Gitea</p>
|
||||
</div>
|
||||
{% if error %}
|
||||
<p class="login-error" role="alert">⚠️ {{ error }}</p>
|
||||
<p class="login-error" role="alert">{{ error }}</p>
|
||||
{% endif %}
|
||||
<form method="post" action="/login" class="login-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<main class="focus-main">
|
||||
{% if not user %}
|
||||
<div class="empty-state">🔑 Aucun utilisateur Gitea résolu : définissez <code>GITEA_TOKEN</code> pour voir les issues qui vous sont assignées.</div>
|
||||
<div class="empty-state">Aucun utilisateur Gitea résolu : définissez <code>GITEA_TOKEN</code> pour voir les issues qui vous sont assignées.</div>
|
||||
{% else %}
|
||||
<p class="focus-intro">Issues ouvertes assignées à <strong>{{ user.login }}</strong>, tous projets confondus — {{ total }} au total.</p>
|
||||
|
||||
@@ -58,11 +58,11 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="empty-state" id="my-empty" hidden>🔍 Aucune issue ne correspond à ces filtres.
|
||||
<div class="empty-state" id="my-empty" hidden>Aucune issue ne correspond à ces filtres.
|
||||
<button type="button" id="my-empty-reset" class="btn-sm">↺ Réinitialiser les filtres</button>
|
||||
</div>
|
||||
{% if not groups %}
|
||||
<div class="empty-state">🎉 Aucune issue ne vous est assignée pour le moment.</div>
|
||||
<div class="empty-state">Aucune issue ne vous est assignée pour le moment.</div>
|
||||
{% endif %}
|
||||
{% for group in groups %}
|
||||
<section class="focus-group" data-repo="{{ group.full_name }}">
|
||||
@@ -81,9 +81,9 @@
|
||||
<a class="issue-main" href="/board/{{ group.full_name }}">
|
||||
<span class="card-id">#{{ issue.number }}</span> {{ issue.title }}
|
||||
</a>
|
||||
<span class="col-chip" style="background: {{ column_colors[row.column] }}20; color: {{ column_colors[row.column] }}">{{ column_labels[row.column] }}</span>
|
||||
<span class="col-chip" style="background: color-mix(in srgb, {{ column_colors[row.column] }} 25%, transparent); color: var(--text)">{{ column_labels[row.column] }}</span>
|
||||
{% for label in issue.labels or [] %}
|
||||
<span class="label-tag" style="background: #{{ label.color }}30; color: #{{ label.color }}">{{ label.name }}</span>
|
||||
<span class="label-tag" style="background: color-mix(in srgb, #{{ label.color }} 25%, transparent); color: var(--text)">{{ label.name }}</span>
|
||||
{% endfor %}
|
||||
<span class="activity-date">{{ (issue.updated_at or '')[:10] }}</span>
|
||||
</div>
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
|
||||
<main class="focus-main">
|
||||
<div class="page-head">
|
||||
<h1>👤 Mon profil</h1>
|
||||
<h1>Mon profil</h1>
|
||||
<p class="page-sub">{{ profile_user.username }} · rôle : {{ profile_user.role }}</p>
|
||||
</div>
|
||||
{% if msg %}<p class="chip-ok">✅ {{ msg }}</p>{% endif %}
|
||||
{% if err %}<p class="chip-warn">⚠️ {{ err }}</p>{% endif %}
|
||||
{% if profile_user.must_change_password %}<p class="chip-warn">⚠️ Ton mot de passe est temporaire : change-le ci-dessous.</p>{% endif %}
|
||||
{% if err %}<p class="chip-warn">{{ err }}</p>{% endif %}
|
||||
{% if profile_user.must_change_password %}<p class="chip-warn">Ton mot de passe est temporaire : change-le ci-dessous.</p>{% endif %}
|
||||
|
||||
<section class="briefing-card">
|
||||
<h2>Informations</h2>
|
||||
@@ -33,7 +33,7 @@
|
||||
</section>
|
||||
|
||||
<section class="briefing-card">
|
||||
<h2>🔑 Mot de passe</h2>
|
||||
<h2>Mot de passe</h2>
|
||||
{% if profile_user.id %}
|
||||
<form method="post" action="/profile/password" class="admin-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
@@ -105,7 +105,7 @@
|
||||
<span class="muted">({{ c.slug }}) · {{ c.base_url }} · compte : {{ c.account_login or 'à tester' }} · jeton : {{ c.token_mask }}</span>
|
||||
<form method="post" action="/profile/connections/{{ c.id }}/test" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" class="btn-mini">🧪 Tester</button>
|
||||
<button type="submit" class="btn-mini">Tester</button>
|
||||
</form>
|
||||
<form method="post" action="/profile/connections/{{ c.id }}/delete" class="inline-form" onsubmit="return confirm('Supprimer cette connexion personnelle ?');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<main class="project-main">
|
||||
<div class="proj-head">
|
||||
<div>
|
||||
<h1>📦 {{ repo_info.name }}</h1>
|
||||
<h1>{{ repo_info.name }}</h1>
|
||||
<p class="proj-sub">{{ owner }} · {{ repo_info.description or 'Aucune description dans Gitea.' }}</p>
|
||||
</div>
|
||||
{% if health_dot %}
|
||||
@@ -26,9 +26,9 @@
|
||||
</div>
|
||||
|
||||
<div class="proj-actions">
|
||||
<a href="/board/{{ full_name }}" class="nav-pill proj-primary">📋 Ouvrir le board</a>
|
||||
<a href="/board/{{ full_name }}" class="nav-pill proj-primary">Ouvrir le board</a>
|
||||
<a href="/capture" class="nav-pill">⚡ Capture</a>
|
||||
<a href="/triage/{{ full_name }}" class="nav-pill">📥 Triage</a>
|
||||
<a href="/triage/{{ full_name }}" class="nav-pill">Triage</a>
|
||||
<a href="/health/{{ full_name }}" class="nav-pill">🩺 Santé</a>
|
||||
<a href="/forecast" class="nav-pill">🔮 Prévisions</a>
|
||||
<a href="/wall?repo={{ full_name }}" class="nav-pill">🖥️ Wallboard</a>
|
||||
@@ -45,7 +45,7 @@
|
||||
<div class="proj-ms">
|
||||
<div class="ms-line">
|
||||
<a href="/board/{{ full_name }}?milestone={{ ms.title|urlencode }}"><strong>{{ ms.title }}</strong></a>
|
||||
{% if ms.due_label %}<span class="ms-due{% if ms.late %} late{% endif %}">{{ '⚠️ ' if ms.late }}échéance {{ ms.due_label }}</span>{% endif %}
|
||||
{% if ms.due_label %}<span class="ms-due{% if ms.late %} late{% endif %}">{{ '' if ms.late }}échéance {{ ms.due_label }}</span>{% endif %}
|
||||
</div>
|
||||
{% if ms.total %}
|
||||
<div class="progress ms-progress"><span style="width: {{ ms.pct }}%"></span></div>
|
||||
@@ -92,7 +92,7 @@
|
||||
<h2>🩺 Santé du flux</h2>
|
||||
{% if health %}
|
||||
<ul class="proj-list">
|
||||
<li>📂 <strong>{{ health.open_count }}</strong> cartes ouvertes</li>
|
||||
<li><strong>{{ health.open_count }}</strong> cartes ouvertes</li>
|
||||
{% if health.slowest %}
|
||||
<li>🐢 Goulot : <strong>{{ COLUMN_LABELS[health.slowest.column] }}</strong> — séjour médian {{ health.slowest.median_days }} j</li>
|
||||
{% else %}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<main class="focus-main">
|
||||
<section class="briefing-card">
|
||||
<h1>⚙️ Règles d'automatisation</h1>
|
||||
<h1>Règles d'automatisation</h1>
|
||||
<p>Des réflexes déclaratifs, évalués toutes les 15 minutes par le planificateur (et à la demande ci-dessous). Chaque action est journalisée comme événement <code>rule</code>. <strong>Garde-fou v1 : une règle ne peut ni fermer ni déplacer une carte entre colonnes</strong> — seulement étiqueter, notifier (ntfy) et remonter en tête de colonne.</p>
|
||||
{% if not configured %}
|
||||
<p>Aucune règle configurée. Définis <code>KANBANTE_RULES</code> en JSON, par exemple :</p>
|
||||
@@ -25,7 +25,7 @@
|
||||
<p class="notes-hint">Conditions : <code>column</code>, <code>min_age_days</code> (temps dans la colonne), <code>label</code> (présente), <code>wip_over</code> (colonne au-delà de N cartes). Actions : <code>add_label</code>, <code>notify</code>, <code>move_top</code>.</p>
|
||||
{% endif %}
|
||||
{% for err in errors %}
|
||||
<p class="chip-warn">⚠️ {{ err }}</p>
|
||||
<p class="chip-warn">{{ err }}</p>
|
||||
{% endfor %}
|
||||
<p><button type="button" class="btn-save btn-inline" id="rules-run">▶️ Évaluer maintenant</button>
|
||||
<span id="rules-run-result" class="notes-hint"></span></p>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{% if q %}
|
||||
<p class="focus-intro">{{ total }} résultat(s) pour « {{ q }} » — recherche Gitea, tous projets confondus.</p>
|
||||
{% if not groups %}
|
||||
<div class="empty-state">🔍 Aucune issue ne correspond. Essayez un mot plus court ou vérifiez l'orthographe.</div>
|
||||
<div class="empty-state">Aucune issue ne correspond. Essayez un mot plus court ou vérifiez l'orthographe.</div>
|
||||
{% endif %}
|
||||
{% for group in groups %}
|
||||
<section class="focus-group">
|
||||
@@ -34,9 +34,9 @@
|
||||
<a class="issue-main" href="/board/{{ group.full_name }}">
|
||||
<span class="card-id">#{{ issue.number }}</span> {{ issue.title }}
|
||||
</a>
|
||||
<span class="col-chip" style="background: {{ column_colors[row.column] }}20; color: {{ column_colors[row.column] }}">{{ column_labels[row.column] }}</span>
|
||||
<span class="col-chip" style="background: color-mix(in srgb, {{ column_colors[row.column] }} 25%, transparent); color: var(--text)">{{ column_labels[row.column] }}</span>
|
||||
{% for label in issue.labels or [] %}
|
||||
<span class="label-tag" style="background: #{{ label.color }}30; color: #{{ label.color }}">{{ label.name }}</span>
|
||||
<span class="label-tag" style="background: color-mix(in srgb, #{{ label.color }} 25%, transparent); color: var(--text)">{{ label.name }}</span>
|
||||
{% endfor %}
|
||||
<span class="activity-date">{{ (issue.updated_at or '')[:10] }}</span>
|
||||
</div>
|
||||
@@ -45,7 +45,7 @@
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="empty-state">🔍 Tapez un mot-clé : la recherche couvre les titres et corps d'issues de <strong>tous</strong> vos projets Gitea, ouverts comme fermés.</div>
|
||||
<div class="empty-state">Tapez un mot-clé : la recherche couvre les titres et corps d'issues de <strong>tous</strong> vos projets Gitea, ouverts comme fermés.</div>
|
||||
{% endif %}
|
||||
</main>
|
||||
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
<main class="focus-main">
|
||||
{% if markdown %}
|
||||
<section class="briefing-card">
|
||||
<h1>📖 {{ milestone_title }}</h1>
|
||||
<h1>{{ milestone_title }}</h1>
|
||||
<p>
|
||||
<button type="button" class="btn-save btn-inline" id="sprint-copy">📋 Copier le markdown</button>
|
||||
<button type="button" class="btn-save btn-inline" id="sprint-download">⬇️ Télécharger (.md)</button>
|
||||
<button type="button" class="btn-save btn-inline" id="sprint-copy">Copier le markdown</button>
|
||||
<button type="button" class="btn-save btn-inline" id="sprint-download">Télécharger (.md)</button>
|
||||
<a class="btn-save btn-inline" style="text-decoration:none" href="/board/{{ full_name }}?milestone={{ milestone_param|urlencode }}">🎯 Voir le board du milestone</a>
|
||||
</p>
|
||||
<p class="notes-hint">Le texte est archivable tel quel dans ObsiGate — c'est le markdown ci-dessous, rendu pour lecture.</p>
|
||||
@@ -45,7 +45,7 @@
|
||||
</script>
|
||||
{% else %}
|
||||
<section class="briefing-card">
|
||||
<h1>📖 Récit de sprint — {{ full_name }}</h1>
|
||||
<h1>Récit de sprint — {{ full_name }}</h1>
|
||||
<p>Choisis un milestone pour générer son récit :</p>
|
||||
<ul>
|
||||
{% for m in milestones %}
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
|
||||
<main class="focus-main">
|
||||
<section class="briefing-card">
|
||||
<h1>🗣️ Standup automatique</h1>
|
||||
<h1>Standup automatique</h1>
|
||||
<p>Écrit par le journal, pas par la mémoire : fermé/déplacé <strong>hier</strong>, focus <strong>aujourd'hui</strong>, cartes qui vieillissent en QA/Révision en <strong>blocages</strong>.</p>
|
||||
<p>
|
||||
<button type="button" class="btn-save btn-inline" id="standup-copy">📋 Copier le standup</button>
|
||||
<button type="button" class="btn-save btn-inline" id="standup-copy">Copier le standup</button>
|
||||
{% if include_in_briefing %}
|
||||
<span class="chip-ok">Inclus dans le briefing ntfy du matin ✔</span>
|
||||
{% else %}
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
{% include "_header.html" %}
|
||||
|
||||
<main class="today-main">
|
||||
<h1 class="today-title">🌅 {{ date_label }}</h1>
|
||||
<p class="today-sub">Trois cartes, pas une de plus. C'est le plan de la journée.</p>
|
||||
<div class="page-head"><div class="page-title"><h1 class="today-title">{{ date_label }}</h1><p class="page-sub">Trois cartes, pas une de plus. C'est le plan de la journée.</p></div></div>
|
||||
|
||||
{% if picks %}
|
||||
<section class="today-picks">
|
||||
@@ -22,18 +21,18 @@
|
||||
<span class="today-card-repo">{{ pick.repo }}</span>
|
||||
<span class="today-card-col">{{ pick.column_label }}</span>
|
||||
</div>
|
||||
<h2>{% if pick.state == 'closed' %}🎉 {% endif %}#{{ pick.number }} — {{ pick.title }}</h2>
|
||||
<h2>{% if pick.state == 'closed' %}{% endif %}#{{ pick.number }} — {{ pick.title }}</h2>
|
||||
<div class="today-card-actions">
|
||||
<a href="/board/{{ pick.repo }}">📋 Ouvrir le board</a>
|
||||
<a href="/board/{{ pick.repo }}">Ouvrir le board</a>
|
||||
{% if pick.html_url %}<a href="{{ pick.html_url }}" target="_blank" rel="noopener">Gitea ↗</a>{% endif %}
|
||||
<button type="button" class="btn-sm" data-unpin data-repo="{{ pick.repo }}" data-number="{{ pick.number }}">📌 Retirer</button>
|
||||
<button type="button" class="btn-sm" data-unpin data-repo="{{ pick.repo }}" data-number="{{ pick.number }}">Retirer</button>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% else %}
|
||||
<section class="briefing-card">
|
||||
<p>Aucune carte épinglée pour aujourd'hui. Épingle-en jusqu'à 3 depuis un board (bouton 📌 sur les cartes) ou depuis les candidates ci-dessous.</p>
|
||||
<p>Aucune carte épinglée pour aujourd'hui. Épingle-en jusqu'à 3 depuis un board (bouton épingler sur les cartes) ou depuis les candidates ci-dessous.</p>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
@@ -55,7 +54,7 @@
|
||||
{% for pick in carry_over %}
|
||||
<li>
|
||||
<span>{{ pick.repo }} #{{ pick.number }} — {{ pick.title }} <em>({{ pick.column_label }})</em></span>
|
||||
{% if picks|length < 3 %}<button type="button" class="btn-sm" data-pin data-repo="{{ pick.repo }}" data-number="{{ pick.number }}">📌 Reprendre aujourd'hui</button>{% endif %}
|
||||
{% if picks|length < 3 %}<button type="button" class="btn-sm" data-pin data-repo="{{ pick.repo }}" data-number="{{ pick.number }}">Reprendre aujourd'hui</button>{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@@ -69,7 +68,7 @@
|
||||
{% for card in candidates %}
|
||||
<li>
|
||||
<span>{{ card.repo }} #{{ card.number }} — {{ card.title }} <em>({{ card.column_label }})</em></span>
|
||||
<button type="button" class="btn-sm" data-pin data-repo="{{ card.repo }}" data-number="{{ card.number }}">📌 Épingler</button>
|
||||
<button type="button" class="btn-sm" data-pin data-repo="{{ card.repo }}" data-number="{{ card.number }}">Épingler</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
@@ -12,20 +12,20 @@
|
||||
|
||||
<main class="focus-main">
|
||||
<section class="briefing-card">
|
||||
<h1>📥 Mode Triage</h1>
|
||||
<h1>Mode Triage</h1>
|
||||
<p>Le backlog se vide carte par carte, au clavier : une décision par carte, aucune souris requise. Choisis un projet pour commencer une session.</p>
|
||||
</section>
|
||||
{% if projects %}
|
||||
<div class="triage-projects">
|
||||
{% for p in projects %}
|
||||
<a class="triage-project" href="/triage/{{ p.full_name }}">
|
||||
<span class="triage-project-name">📂 {{ p.full_name }}</span>
|
||||
<span class="triage-project-name">{{ p.full_name }}</span>
|
||||
<span class="triage-project-count">{{ p.backlog }} carte{{ 's' if p.backlog > 1 }} au backlog</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="briefing-card"><p>🎉 Aucun backlog à trier : tous les projets actifs sont à zéro. Inbox zero, déjà.</p></div>
|
||||
<div class="briefing-card"><p>Aucun backlog à trier : tous les projets actifs sont à zéro. Inbox zero, déjà.</p></div>
|
||||
{% endif %}
|
||||
</main>
|
||||
<footer>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<main class="triage-main" data-repo="{{ full_name }}">
|
||||
<div class="triage-head">
|
||||
<h1>📥 Triage — {{ full_name }}</h1>
|
||||
<h1>Triage — {{ full_name }}</h1>
|
||||
<div class="triage-progress-wrap">
|
||||
<div class="triage-progress"><div class="triage-progress-bar" id="triage-bar"></div></div>
|
||||
<span class="triage-progress-label" id="triage-progress-label"></span>
|
||||
@@ -32,10 +32,10 @@
|
||||
</section>
|
||||
|
||||
<section class="triage-done" id="triage-done" hidden>
|
||||
<h2>🎉 Session terminée</h2>
|
||||
<h2>Session terminée</h2>
|
||||
<ul class="triage-summary" id="triage-summary"></ul>
|
||||
<p>
|
||||
<a class="btn-save btn-inline" href="/board/{{ full_name }}">📋 Retour au board</a>
|
||||
<a class="btn-save btn-inline" href="/board/{{ full_name }}">Retour au board</a>
|
||||
<a class="btn-save btn-inline" href="/triage/{{ full_name }}">🔁 Nouvelle session</a>
|
||||
</p>
|
||||
</section>
|
||||
@@ -43,9 +43,9 @@
|
||||
<div class="triage-status" id="triage-status" role="status"></div>
|
||||
|
||||
<section class="triage-keys" id="triage-keys">
|
||||
<button type="button" data-act="move" data-col="todo"><kbd>1</kbd> 📋 À faire</button>
|
||||
<button type="button" data-act="move" data-col="todo"><kbd>1</kbd> À faire</button>
|
||||
<button type="button" data-act="move" data-col="in_progress"><kbd>2</kbd> 🚧 En cours</button>
|
||||
<button type="button" data-act="move" data-col="qa"><kbd>3</kbd> 🧪 QA</button>
|
||||
<button type="button" data-act="move" data-col="qa"><kbd>3</kbd> QA</button>
|
||||
<button type="button" data-act="move" data-col="review"><kbd>4</kbd> 👀 Révision</button>
|
||||
<button type="button" data-act="move" data-col="done"><kbd>5</kbd> ✅ Terminé</button>
|
||||
<button type="button" data-act="labels"><kbd>L</kbd> 🏷️ Étiquettes</button>
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script>try{var t=localStorage.getItem("kanbante-theme");document.documentElement.setAttribute("data-theme",t==="light"?"light":"dark");}catch(e){document.documentElement.setAttribute("data-theme","dark");}</script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Kanbante — Wallboard</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
@@ -13,7 +14,7 @@
|
||||
<section class="wall-slide{% if loop.first %} active{% endif %}">
|
||||
<header class="wall-head">
|
||||
<div>
|
||||
<div class="wall-kicker">📂 Projet</div>
|
||||
<div class="wall-kicker">Projet</div>
|
||||
<h1 class="wall-project">{{ slide.full_name }}</h1>
|
||||
</div>
|
||||
<div class="wall-clock">
|
||||
@@ -41,7 +42,7 @@
|
||||
<span class="wall-card-col" style="--col: {{ column_colors[card.column] }}">{{ card.column_label }}</span>
|
||||
<span class="wall-card-title">#{{ card.number }} · {{ card.title }}</span>
|
||||
{% if card.age_days is not none and card.age_days >= 1 %}
|
||||
<span class="wall-card-age{% if card.aging %} hot{% endif %}">⏳ {{ card.age_days }} j</span>
|
||||
<span class="wall-card-age{% if card.aging %} hot{% endif %}">{{ card.age_days }} j</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
@@ -50,12 +51,12 @@
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="wall-idle">🌿 Rien en vol pour le moment — tout est au Backlog ou terminé.</p>
|
||||
<p class="wall-idle">Rien en vol pour le moment — tout est au Backlog ou terminé.</p>
|
||||
{% endif %}
|
||||
|
||||
<footer class="wall-foot">
|
||||
<span>✅ Fermées aujourd'hui : <strong>{{ slide.closed_today }}</strong></span>
|
||||
<span>📋 Ouvertes : <strong>{{ slide.open_total }}</strong></span>
|
||||
<span>Ouvertes : <strong>{{ slide.open_total }}</strong></span>
|
||||
<span class="wall-live">● EN DIRECT</span>
|
||||
</footer>
|
||||
</section>
|
||||
@@ -71,7 +72,7 @@
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="wall-empty">
|
||||
<h1>🖥️ Wallboard</h1>
|
||||
<h1>Wallboard</h1>
|
||||
<p>Aucun projet actif à afficher{% if single %} pour ce dépôt{% endif %}.</p>
|
||||
<p class="wall-empty-hint">Astuce : <code>/wall?repo=Proprietaire/depot</code> fige un seul projet.</p>
|
||||
</div>
|
||||
|
||||
+332
-172
File diff suppressed because it is too large
Load Diff
@@ -176,6 +176,47 @@
|
||||
if (e.key === 'Escape') closeMenus();
|
||||
});
|
||||
|
||||
/* ── Copier l'URL du projet (#44) ─────────────────────── */
|
||||
function showCopyToast(message) {
|
||||
var toast = document.createElement('div');
|
||||
toast.className = 'kanbante-toast';
|
||||
toast.setAttribute('role', 'status');
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(function () { toast.remove(); }, 2600);
|
||||
}
|
||||
function copyTextFallback(text) {
|
||||
var area = document.createElement('textarea');
|
||||
area.value = text;
|
||||
area.style.cssText = 'position:fixed;opacity:0;pointer-events:none;';
|
||||
document.body.appendChild(area);
|
||||
area.select();
|
||||
var ok = false;
|
||||
try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
|
||||
area.remove();
|
||||
return ok;
|
||||
}
|
||||
function copyText(text) {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
return navigator.clipboard.writeText(text).then(
|
||||
function () { return true; },
|
||||
function () { return copyTextFallback(text); }
|
||||
);
|
||||
}
|
||||
return Promise.resolve(copyTextFallback(text));
|
||||
}
|
||||
document.querySelectorAll('[data-copy-url]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
var url = btn.getAttribute('data-copy-url') || '';
|
||||
if (!url) return;
|
||||
copyText(url).then(function (ok) {
|
||||
closeMenus();
|
||||
showCopyToast(ok ? 'URL du projet copiée' : 'La copie a échoué');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* Le tri des tableaux denses (#22) vit dans tablesort.js,
|
||||
partagé avec /forecast et /health (0.19.0). */
|
||||
})();
|
||||
|
||||
@@ -34,6 +34,35 @@
|
||||
if (window.innerWidth > 900) closeDrawer();
|
||||
});
|
||||
|
||||
/* Bascule Dark/Light (#52) : le choix est mémorisé et
|
||||
l'amorce de _header.html l'a déjà appliqué avant le rendu.
|
||||
Ici, on ne fait que synchroniser le libellé et le clic. */
|
||||
var themeBtn = document.getElementById('theme-toggle');
|
||||
function syncThemeBtn(theme) {
|
||||
if (!themeBtn) return;
|
||||
var light = theme === 'light';
|
||||
/* Bouton dans la section Administration (#55) : icône et
|
||||
libellé sont des <span> séparés ; l'ancien bouton de
|
||||
l'en-tête, lui, n'avait qu'un texte. */
|
||||
var ico = themeBtn.querySelector('[data-theme-ico]');
|
||||
var lbl = themeBtn.querySelector('[data-theme-label]');
|
||||
if (ico) ico.textContent = light ? '🌙' : '☀️';
|
||||
if (lbl) lbl.textContent = light ? 'Mode sombre' : 'Mode clair';
|
||||
if (!ico && !lbl) {
|
||||
themeBtn.textContent = light ? '🌙 Mode sombre' : '☀️ Mode clair';
|
||||
}
|
||||
themeBtn.setAttribute('aria-pressed', light ? 'true' : 'false');
|
||||
}
|
||||
if (themeBtn) {
|
||||
syncThemeBtn(document.documentElement.getAttribute('data-theme') || 'dark');
|
||||
themeBtn.addEventListener('click', function () {
|
||||
var next = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
|
||||
document.documentElement.setAttribute('data-theme', next);
|
||||
try { localStorage.setItem('kanbante-theme', next); } catch (e) { /* non-fatal */ }
|
||||
syncThemeBtn(next);
|
||||
});
|
||||
}
|
||||
|
||||
if (collapseBtn) {
|
||||
collapseBtn.addEventListener('click', function () {
|
||||
var collapsed = document.body.classList.toggle('sidebar-collapsed');
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Menu ⋯ des tuiles projets : copie d'URL (#44) et thème
|
||||
opaque barre Fusion / Spark, sombre et clair (#45).
|
||||
|
||||
Le menu était transparent parce que son fond utilisait
|
||||
var(--surface-1), variable jamais définie dans :root.
|
||||
"""
|
||||
|
||||
|
||||
def test_card_menu_offers_copy_url(client, fake_gitea):
|
||||
page = client.get("/")
|
||||
assert "Copier l'URL du projet" in page.text
|
||||
assert 'data-copy-url="http://gitea.test/Projets/demo"' in page.text
|
||||
|
||||
|
||||
def test_card_menu_theme_is_opaque_and_dual(client, fake_gitea):
|
||||
css = client.get("/static/css/style.css").text
|
||||
# Le menu ne repose plus sur la variable indéfinie.
|
||||
menu = css.split(".card-menu {", 1)[1].split("}", 1)[0]
|
||||
assert "var(--surface-1)" not in menu
|
||||
assert "var(--menu-bg)" in menu
|
||||
# Jetons barre Fusion / Console v7.2, clair par défaut…
|
||||
assert "--menu-bg: #ffffff" in css
|
||||
# …et variante sombre explicite, comme draco-barre.css.
|
||||
assert '[data-theme="dark"] .card-menu' in css
|
||||
assert "--menu-bg: #1a1c1a" in css
|
||||
assert "prefers-color-scheme: dark" in css
|
||||
|
||||
|
||||
def test_dashboard_js_copy_contract(client, fake_gitea):
|
||||
js = client.get("/static/js/dashboard.js").text
|
||||
assert "data-copy-url" in js
|
||||
assert "navigator.clipboard" in js
|
||||
assert "execCommand('copy')" in js
|
||||
assert "URL du projet copiée" in js
|
||||
@@ -0,0 +1,12 @@
|
||||
"""Page Projets (#47) : les fichiers qui portent le menu ⋯ et la
|
||||
copie d'URL doivent être versionnés, sinon le navigateur garde
|
||||
l'ancien CSS/JS après une release alors que le HTML est neuf —
|
||||
c'est exactement ce qui a fait paraître la 0.38.0 inefficace
|
||||
(menu toujours transparent, bouton de copie présent mais inerte).
|
||||
"""
|
||||
|
||||
|
||||
def test_dashboard_static_assets_are_versioned(client, fake_gitea):
|
||||
page = client.get("/")
|
||||
assert "/static/css/style.css?v=" in page.text
|
||||
assert "/static/js/dashboard.js?v=" in page.text
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Identité Kanbante (#54, #55) : sidebar charbon (pas bleutée),
|
||||
accent vert Kanbante (pas rose Spark), icônes de navigation en
|
||||
trait (pas d'emoji), bouton Dark/Light dans la section
|
||||
Administration (pas dans l'en-tête).
|
||||
"""
|
||||
|
||||
|
||||
def test_sidebar_charbon_et_accent_vert(client, fake_gitea):
|
||||
css = client.get("/static/css/style.css").text
|
||||
assert "rgba(26, 28, 26, 0.96)" in css
|
||||
assert "rgba(13, 16, 24" not in css
|
||||
assert "--accent: #0d9488;" in css
|
||||
assert "#c2437e" not in css
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def test_theme_toggle_dans_admin_pas_dans_en_tete(client, fake_gitea):
|
||||
page = client.get("/")
|
||||
assert 'id="theme-toggle"' in page.text
|
||||
head = page.text.split("header-actions", 1)[1].split("</header>", 1)[0]
|
||||
assert 'id="theme-toggle"' not in head
|
||||
# Placement administrateur : le bouton est dans le bloc de la
|
||||
# section Administration du gabarit (les non-admins, eux, le
|
||||
# voient dans le pied de la barre, la section n'existant pas
|
||||
# pour eux — sans quoi ils perdraient le basculeur).
|
||||
tpl = open("app/templates/_header.html", encoding="utf-8").read()
|
||||
admin_block = tpl.split("gkey == 'admin'", 1)[1]
|
||||
assert 'id="theme-toggle"' in admin_block
|
||||
assert "data-theme-label" in admin_block
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Navigation (#51) et bascule Dark/Light (#52).
|
||||
|
||||
L'accordéon de la barre latérale omettait la section admin :
|
||||
le JS posait nav-closed-admin, mais aucun style ne la suivait.
|
||||
Le thème clair, lui, doit être atteignable par un bouton et
|
||||
mémorisé, avec les jetons draco clairs.
|
||||
"""
|
||||
|
||||
|
||||
def test_admin_accordion_closes(client, fake_gitea):
|
||||
css = client.get("/static/css/style.css").text
|
||||
assert 'body.nav-closed-admin [data-group="admin"] .side-acc' in css
|
||||
assert 'body.nav-closed-admin [data-group="admin"] .side-chevron' in css
|
||||
|
||||
|
||||
def test_theme_toggle_present_and_versioned(client, fake_gitea):
|
||||
page = client.get("/")
|
||||
assert 'id="theme-toggle"' in page.text
|
||||
assert "/static/js/nav.js?v=" in page.text
|
||||
# #55 : le bouton n'est plus dans l'en-tête applicatif.
|
||||
head = page.text.split("header-actions", 1)[1].split("</header>", 1)[0]
|
||||
assert 'id="theme-toggle"' not in head
|
||||
|
||||
|
||||
def test_theme_js_contract(client, fake_gitea):
|
||||
js = client.get("/static/js/nav.js").text
|
||||
assert "kanbante-theme" in js
|
||||
assert "theme-toggle" in js
|
||||
assert "data-theme" in js
|
||||
|
||||
|
||||
def test_light_theme_tokens(client, fake_gitea):
|
||||
css = client.get("/static/css/style.css").text
|
||||
assert 'html[data-theme="light"]' in css
|
||||
assert "--bg: #f2f2f0;" in css
|
||||
assert "--surface: #ffffff;" in css
|
||||
assert "--accent-2: #0f766e;" in css
|
||||
assert 'html[data-theme="light"] .draco-bar' in css
|
||||
assert 'html[data-theme="light"] .card-menu' in css
|
||||
@@ -25,7 +25,7 @@ def test_sidebar_on_app_pages(client, fake_gitea):
|
||||
# Bouton palette : icône seule, à droite du titre — pas de
|
||||
# libellé texte « Recherche & commandes » visible.
|
||||
assert 'class="side-head"' in page.text
|
||||
assert ">🔍</button>" in page.text
|
||||
assert 'class="side-search palette-trigger"' in page.text
|
||||
# Le libellé de l'ancien bouton palette a disparu (le lien de
|
||||
# navigation « Recherche », lui, existe toujours — c'est le
|
||||
# texte complet « Recherche & commandes » qui doit manquer).
|
||||
@@ -89,4 +89,4 @@ def test_wall_more_cards_line(client, fake_gitea, monkeypatch):
|
||||
page = client.get("/wall")
|
||||
assert page.status_code == 200
|
||||
assert "+3 autres cartes en vol" in page.text
|
||||
assert "📋 Ouvertes" in page.text
|
||||
assert "Ouvertes" in page.text
|
||||
|
||||
@@ -197,7 +197,7 @@ def test_board_filter_by_milestone_id(client):
|
||||
def test_dashboard_uses_new_design_system(client):
|
||||
r = client.get("/")
|
||||
assert "app-header" in r.text
|
||||
assert "nav-pill" in r.text
|
||||
assert "seg-tabs" in r.text
|
||||
# En-tête refondu en 0.20.0 : bande KPI unifiée (les stat-tile
|
||||
# ne servent plus qu'au briefing).
|
||||
assert "kpi-strip" in r.text
|
||||
|
||||
@@ -8,7 +8,7 @@ from app import models
|
||||
def test_dashboard_sort_offers_review_mode(client):
|
||||
r = client.get("/")
|
||||
assert r.status_code == 200
|
||||
assert '<option value="review">🔍 En attente de revue ↓</option>' in r.text
|
||||
assert '<option value="review">En attente de revue ↓</option>' in r.text
|
||||
|
||||
|
||||
def test_dashboard_cards_carry_review_count(client):
|
||||
|
||||
+16
-1
@@ -34,7 +34,7 @@ def test_origin_of():
|
||||
|
||||
|
||||
def test_sister_origins_dev():
|
||||
# Correctif 2026-10-28 : 8 origines officielles, prod ET dev,
|
||||
# Correctif 2026-10-28 : 10 origines officielles (8 sœurs + 2 agent), prod ET dev,
|
||||
# quelle que soit la résolution.
|
||||
assert sister_origins(RESOLVED_DEV) == {
|
||||
"https://og.dracodev.net",
|
||||
@@ -45,9 +45,24 @@ def test_sister_origins_dev():
|
||||
"https://flowdeck-dev.dracodev.net",
|
||||
"https://tiktak-dev.dracodev.net",
|
||||
"https://kanbante-dev.dracodev.net",
|
||||
"https://agent.dracodev.net",
|
||||
"https://agent-dev.dracodev.net",
|
||||
}
|
||||
|
||||
|
||||
|
||||
def test_cors_headers_agent_console_autorise():
|
||||
# Fédération depuis la console (2026-10-28) : les origines agent
|
||||
# prod ET dev passent, avec credentials ; un tiers reste refusé.
|
||||
for origine in ("https://agent.dracodev.net",
|
||||
"https://agent-dev.dracodev.net"):
|
||||
headers = cors_headers_for(origine, RESOLVED_DEV)
|
||||
assert headers is not None
|
||||
assert headers["Access-Control-Allow-Origin"] == origine
|
||||
assert headers["Access-Control-Allow-Credentials"] == "true"
|
||||
assert cors_headers_for("https://agent.dracodev.net.evil.example",
|
||||
RESOLVED_DEV) is None
|
||||
|
||||
def test_cors_headers():
|
||||
headers = cors_headers_for("https://tiktak-dev.dracodev.net", RESOLVED_DEV)
|
||||
assert headers["Access-Control-Allow-Origin"] == "https://tiktak-dev.dracodev.net"
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Thème du site (#49, #54) : Kanbante doit ressembler à la console
|
||||
agent / barre Fusion (draco-barre, Console v7.2), pas au bleu/mauve
|
||||
historique — et l'accent est désormais le vert Kanbante, pas le
|
||||
rose Spark. Le test fige les jetons et interdit le retour des
|
||||
littéraux rose/violet/bleu de thème dans style.css.
|
||||
"""
|
||||
|
||||
|
||||
def test_theme_agent_tokens(client, fake_gitea):
|
||||
css = client.get("/static/css/style.css").text
|
||||
assert "--bg: #101211;" in css
|
||||
assert "--surface: #1a1c1a;" in css
|
||||
assert "--surface-2: #242624;" in css
|
||||
assert "--surface-3: #20221f;" in css
|
||||
assert "--border: #2e302c;" in css
|
||||
assert "--text: #eceee9;" in css
|
||||
assert "--accent: #0d9488;" in css
|
||||
assert "--accent-2: #14b8a6;" in css
|
||||
|
||||
|
||||
def test_no_blue_purple_theme_literals(client, fake_gitea):
|
||||
css = client.get("/static/css/style.css").text
|
||||
for banned in (
|
||||
"#7c5cfc", "#5b8af7", "#58a6ff", "#a78bfa", "#7dd3fc",
|
||||
"#c4b5fd", "#a5c4fd", "#0b0e14", "#141822", "#1b202e",
|
||||
"#232939", "124, 92, 252", "124,92,252",
|
||||
"91, 138, 247", "91,138,247",
|
||||
"#c2437e", "#e078a8", "194, 67, 126", "194,67,126",
|
||||
"224, 120, 168", "224,120,168",
|
||||
):
|
||||
assert banned not in css, banned
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Lot UX (#57 à #68) : jetons, boutons, accent-text, icônes,
|
||||
thème clair sur connexion/wallboard, en-tête de page commun.
|
||||
"""
|
||||
|
||||
|
||||
def test_tokens_et_accent_text(client, fake_gitea):
|
||||
css = client.get("/static/css/style.css").text
|
||||
for token in (
|
||||
"--accent-text: #14b8a6;",
|
||||
"--accent-text: #0f766e;",
|
||||
"--radius-full: 999px;",
|
||||
"--shadow-lg:",
|
||||
"--font-xl: 1.55rem;",
|
||||
):
|
||||
assert token in css
|
||||
assert "linear-gradient(135deg, rgba(13,148,136,.9)" not in css
|
||||
assert '"Inter"' not in css
|
||||
|
||||
|
||||
def test_boutons_et_skeleton(client, fake_gitea):
|
||||
css = client.get("/static/css/style.css").text
|
||||
for cls in (".btn-primary", ".btn-danger", ".btn-icon", ".skeleton", ".field-error-msg"):
|
||||
assert cls in css
|
||||
|
||||
|
||||
def test_cartes_et_labels_lisibles(client, fake_gitea):
|
||||
css = client.get("/static/css/style.css").text
|
||||
assert "-webkit-line-clamp: 2" in css
|
||||
card = open("app/templates/_card.html", encoding="utf-8").read()
|
||||
assert "pin-btn btn-icon" in card
|
||||
assert "color-mix(in srgb" in card
|
||||
assert "📌" not in card
|
||||
|
||||
|
||||
def test_pas_de_recherche_en_double_et_theme_clair_login_wall(client, fake_gitea):
|
||||
header = open("app/templates/_header.html", encoding="utf-8").read()
|
||||
assert "topbar-search" not in header
|
||||
for tpl in ("app/templates/login.html", "app/templates/wall.html"):
|
||||
assert "kanbante-theme" in open(tpl, encoding="utf-8").read()
|
||||
css = client.get("/static/css/style.css").text
|
||||
assert 'html[data-theme="light"] .wall-body' in css
|
||||
assert 'html[data-theme="light"] .login-body' in css
|
||||
|
||||
|
||||
def test_pages_avec_en_tete_commun(client, fake_gitea):
|
||||
today = open("app/templates/today.html", encoding="utf-8").read()
|
||||
focus = open("app/templates/focus.html", encoding="utf-8").read()
|
||||
assert 'class="page-head"' in today
|
||||
assert 'class="page-head"' in focus
|
||||
assert "🌅" not in today
|
||||
Reference in New Issue
Block a user