test_column_entry_times_ignores_comments évaluait _days_ago(3) à la création puis à l'assertion : une bascule de seconde entre les deux faisait échouer le run CI 2333 alors que le run jumeau (tag v0.33.0) et la suite locale passaient.
Kanbante
Kanban project manager integrated with Gitea — lightweight, self-hosted, dark theme.
Features
- 📋 Dashboard — tous tes projets Gitea en un coup d'œil (actifs / inactifs / archivés — le vrai drapeau Gitea)
- 🗂️ Kanban Board — colonnes Backlog → À faire → En cours → QA → Révision → Terminé, ordre des cartes persisté
- 🎯 Drag & Drop — glisse les issues entre colonnes ; en cas d'échec, la carte est replacée
- 🔗 Gitea Sync bidirectionnelle — « Terminé » ferme l'issue dans Gitea, la ressortir la rouvre ; l'état local est réconcilié à chaque chargement
- ➕ Création d'issues depuis le board, filtres texte + étiquettes, cartes liées à Gitea
- ⚡ Temps réel — un webhook Gitea invalide le cache et pousse un événement SSE : le board ouvert se met à jour tout seul (en douceur, sans interrompre une saisie ni un drag)
- 🏷️ Visible dans Gitea — les colonnes intermédiaires sont reflétées par des étiquettes
kanban:*sur les issues, et un board peut se filtrer par milestone (échéance + progression façon sprint) - 🗂️ Panneau détail — clic sur une carte : corps markdown, commentaires, et réponse postée dans Gitea sans quitter le board
- 📰 Journal d'événements — chaque mouvement est horodaté : fil d'activité (
/activity), métriques de débit et de temps de cycle (/api/metrics) - ⏳ Vieillissement + limites WIP — badge d'ancienneté par colonne et limites souples configurables
- 🎯 Vue Focus — tout ce qui est En cours / en QA / En révision, tous projets confondus (
/focus) - 🖥️ Wallboard — mode TV plein écran (
/wall) : rotation automatique entre les projets actifs, cartes en vol et compteurs en très grands caractères, rafraîchi en direct ;?repo=Proprietaire/depotfige un seul projet - 🌅 Focus du jour —
/today: jusqu'à 3 cartes épinglées (bouton 📌 sur les cartes), candidates de l'encours, reprises d'hier non terminées, récapitulatif du soir ; intégrées au briefing - 🔮 Prévisions — débit réel du journal (fermetures/semaine), date d'épuisement du backlog et ETA des milestones en fourchette honnête, sur le dashboard, le bandeau milestone et
/forecast(sparkline SVG) - 📖 Récit de sprint — le bilan markdown d'un milestone (livré, cycles, plus lentes, reporté) généré depuis le journal, page
/sprint/...+ copie/téléchargement en un clic - 🧬 Code lié — branches, PRs et commits référant
#nsur la carte (badge⑂) et dans le panneau détail (section « Code lié ») - 🔀 PR fusionnée → Terminé — le webhook
pull_requestretrouve les issues liées (références#n, brancheissue-n-*), les ferme et déplace leurs cartes, journalisé - 🗣️ Standup automatique —
/standup: fermé/déplacé hier, focus du jour, blocages (QA/Révision qui vieillissent), copiable d'un clic ; ajout optionnel au briefing (KANBANTE_BRIEFING_INCLUDE_STANDUP=1) - ⚙️ Règles d'automatisation — moteur déclaratif
KANBANTE_RULES(JSON) évalué toutes les 15 min : conditions colonne/âge/étiquette/WIP → étiquette, notification ntfy, tête de colonne ; jamais de fermeture par règle ; page/rules - 🌿 Branche depuis la carte — le panneau détail crée
issue-<n>-<slug>dans Gitea depuis la branche par défaut, déplace la carte en « En cours » et journalise ; la PR qui en naîtra sera reconnue automatiquement - 🩺 Santé du flux —
/health: temps médian par colonne (du journal), colonne la plus lente, et cartes zombies (aucune activité depuisKANBANTE_ZOMBIE_DAYS) avec actions rapides (commenter, remettre au backlog, fermer) - 📥 Mode Triage —
/triage: le backlog se vide carte par carte, plein cadre, au clavier (1-5colonnes,Létiquettes,As'assigner,Xfermer,Entréesuivante,Zannuler), progression et résumé de session ; décisions via les mêmes API que le board - 🔄 Board dynamique — aucun refresh manuel : toute modification (autre session, Gitea, capture) est diffusée en SSE et appliquée en place avec animation des cartes (
/api/board/{owner}/{repo}en JSON + fragments rendus serveur, diff DOM/FLIP) ; repli par sondage de 45 s sans webhooks ; drag local, filtres et panneau détail préservés - ⌨️ Palette de commandes —
Ctrl+K(ou le bouton ⌨️) sur toutes les pages : actions, projets et recherche d'issues globale, 100 % clavier - 🧠 Digest hebdo → ObsiGate — résumé markdown de la semaine (journal + focus) composé automatiquement, livré seulement sur approbation (page
/digest) vers le serveur MCP d'ObsiGate (client MCP intégré, outil et gabarit d'arguments configurables) ou un webhook générique - ⚡ Capture éclair — page
/capture(formulaire), endpointPOST /api/captureprotégé par un jeton dédié, et écoute ntfy : un message publié sur le topic de capture devient une issue dans le dépôt par défaut - 🔀 Réconciliation par étiquettes — si l'étiquette
kanban:*d'une issue change hors de Kanbante (dans Gitea, par un autre outil), le board adopte la colonne de l'étiquette au chargement et le journalise (sourcegitea). Une seule étiquettekanban:*fait foi ; aucune ou plusieurs = l'état local gagne - 🔍 Recherche globale + Mes issues — chercher dans toutes les issues de tous les projets (
/search), et voir ce qui vous est assigné (/my) - 📊 Briefing quotidien — encours, vieillissement, milestones proches : page
/briefinget envoi ntfy planifié - 🤖 Serveur MCP — 7 outils de lecture (boards, focus, recherche, métriques) + 2 écritures minimales (
create_issue,add_comment) exposés en JSON-RPC sur/mcppour vos agents - 🏷️ Étiquettes gérables depuis le board — créer / supprimer les étiquettes du dépôt, les appliquer aux issues depuis le panneau détail
- 📝 Notes de projet — panneau repliable, largeur ajustable, sauvegarde automatique
- 📝 Notes — bloc-notes par projet
- 🔐 Authentification Basic Auth intégrée + protection CSRF (voir Sécurité)
- 🌙 Dark theme — conçu pour les longues sessions
- 🐳 Docker — un seul conteneur, utilisateur non-root, healthcheck
- 📴 Aucun CDN — les bibliothèques JS sont vendorées dans
static/vendor/, l'app fonctionne hors-ligne
Quick Start
# 1. Installer les dépendances
cd kanbante
pip install -e .
# 2. Configurer les variables d'environnement
export GITEA_URL=https://git.dracodev.net
export GITEA_TOKEN=ton_token_gitea
# Recommandé dès que le service dépasse localhost :
export KANBANTE_AUTH_USER=ton_nom
export KANBANTE_AUTH_PASSWORD=ton_mot_de_passe
# 3. Lancer
uvicorn app.main:app --reload --port 8080
Ouvrir http://localhost:8080
Docker
Avec docker-compose (recommandé)
cat > .env <<'EOF'
GITEA_TOKEN=ton_token
KANBANTE_AUTH_USER=ton_nom
KANBANTE_AUTH_PASSWORD=ton_mot_de_passe
EOF
docker compose up -d
La base SQLite vit dans le volume kanbante_data (monté sur /data,
chemin KANBANTE_DB_PATH) : les positions de cartes et les notes
survivent aux recréations du conteneur.
Avec docker uniquement
docker build -t kanbante:latest .
docker volume create kanbante_data
docker run -d \
--name kanbante \
-p 8080:8080 \
-v kanbante_data:/data \
-e GITEA_URL=https://git.dracodev.net \
-e GITEA_TOKEN=ton_token \
-e KANBANTE_AUTH_USER=ton_nom \
-e KANBANTE_AUTH_PASSWORD=ton_mot_de_passe \
--restart unless-stopped \
kanbante:latest
Déploiement sur Deepin 25 (Debian Bookworm)
sudo apt install -y docker.io docker-compose docker-buildx
sudo usermod -aG docker $USER
sudo systemctl enable --now docker
# Builder et lancer (voir section "Avec docker uniquement")
Configuration
| Variable | Défaut | Rôle |
|---|---|---|
GITEA_URL |
https://git.dracodev.net |
Instance Gitea |
GITEA_TOKEN |
(vide) | Jeton API Gitea — portée conseillée : lecture + écriture des issues, sur un compte dédié, pas un jeton admin. Sans jeton : contenu public seulement |
KANBANTE_PUBLIC_URL |
https://kanbante.dracodev.net |
Propre URL publique de Kanbante (lien de la barre commune, return_to du lien SSO) — noms publics par configuration : défauts = noms officiels (prod), la dev déclare ses noms en -dev (ex. https://kanbante-dev.dracodev.net) |
DRACO_SSO_URL |
https://og.dracodev.net |
Base d'ObsiGate, émettrice SSO (lien de barre ObsiGate, révocations, lien de connexion SSO) |
DRACO_FLOWDECK_URL / DRACO_TIKTAK_URL |
https://flowdeck.dracodev.net / https://tiktak.dracodev.net |
Liens Flowdeck / TikTak de la barre commune |
AGENT_URL |
https://agent.dracodev.net |
Base du service d'agents (panneau Spark + console, adresse injectée au JS par l'hôte) — sans variable explicite, dérivée de l'hôte d'AGENT_EVENEMENTS_URL s'il suit agent<SUFFIXE>.dracodev.net ; à chaque requête, l'hôte public (motif fermé *.dracodev.net) détermine l'environnement des URL rendues sauf variable explicite (0.32.2) |
AGENT_EVENEMENTS_URL |
(dérivé de AGENT_URL) |
Journal d'événements du service d'agents ; ne se pose que pour surcharger la dérivation <AGENT_URL>/api/v1/evenements |
AGENT_EVENEMENTS_CLE |
(vide) | Clé HMAC signant les événements émis vers ce journal ; vide = émission désactivée |
KANBANTE_DB_PATH |
~/.kanbante/kanbante.db |
Base SQLite locale (en Docker : /data/kanbante.db) |
KANBANTE_AUTH_USER / KANBANTE_AUTH_PASSWORD |
(vide) | Active l'authentification dès que les deux sont définis : écran de connexion + session signée pour les navigateurs ; l'en-tête Basic reste accepté pour les scripts |
KANBANTE_REQUIRE_AUTH |
0 |
1 = l'app refuse de servir (503) si l'auth n'est pas configurée |
KANBANTE_SESSION_SECRET |
(dérivé) | Clé HMAC du cookie de session ; par défaut dérivée des identifiants (changer le mot de passe invalide les sessions) |
KANBANTE_SNAPSHOT_INTERVAL |
120 |
Secondes entre les rafraîchissements d'instantanés d'arrière-plan (chantier B) ; 0 = désactivé |
KANBANTE_ACTIVE_WINDOW_DAYS |
365 |
Un dépôt non archivé mis à jour depuis moins de N jours est « actif » |
KANBANTE_CACHE_TTL |
30 |
Secondes de cache des listes Gitea (dépôts, issues) |
KANBANTE_MAX_NOTE_CHARS |
20000 |
Taille maximale d'une note de projet |
KANBANTE_SYNC_GITEA |
1 |
0 = les déplacements ne ferment/rouvrent plus les issues Gitea |
KANBANTE_WIP_LIMITS |
(vide) | Limites souples par colonne, ex. in_progress:5,review:3 (affichage n/limite, rouge en dépassement, jamais bloquant) |
KANBANTE_AGING_DAYS |
7 |
Jours dans la même colonne avant le badge « ⏳ » sur une carte (rouge au double) |
KANBANTE_WEBHOOK_SECRET |
(vide) | Secret partagé du webhook Gitea ; si défini, la signature HMAC X-Gitea-Signature est exigée |
KANBANTE_SYNC_LABELS |
1 |
0 = ne plus refléter les colonnes intermédiaires en étiquettes kanban:* dans Gitea |
KANBANTE_NTFY_URL |
(vide) | Base du serveur ntfy (ex. https://ntfy.sh ou auto-hébergé) pour le briefing |
KANBANTE_NTFY_TOPIC |
(vide) | Topic ntfy du briefing (URL + topic requis pour activer l'envoi) |
KANBANTE_NTFY_TOKEN |
(vide) | Jeton Bearer optionnel pour un topic protégé |
KANBANTE_BRIEFING_TIME |
(vide) | Heure d'envoi quotidien du briefing, HH:MM heure du serveur ; vide = pas d'envoi automatique |
KANBANTE_CAPTURE_REPO |
(vide) | Dépôt par défaut de la capture éclair (Proprietaire/depot) |
KANBANTE_CAPTURE_TOKEN |
(vide) | Jeton dédié de POST /api/capture ; vide = endpoint désactivé (503) |
KANBANTE_CAPTURE_NTFY_TOPIC |
(vide) | Topic ntfy écouté : chaque message devient une issue dans KANBANTE_CAPTURE_REPO |
KANBANTE_BRIEFING_INCLUDE_STANDUP |
0 |
1 = le standup automatique est ajouté au briefing ntfy du matin |
KANBANTE_RULES |
(vide) | Règles d'automatisation en JSON (voir /rules) : conditions colonne/âge/étiquette/WIP → add_label / notify / move_top |
KANBANTE_ZOMBIE_DAYS |
30 |
Jours sans activité après lesquels une carte ouverte est « zombie » sur /health |
KANBANTE_DIGEST_TIME / KANBANTE_DIGEST_WEEKDAY |
(vide) / 0 |
Composition hebdo du digest (HH:MM, 0 = lundi) ; notification ntfy, jamais d'envoi automatique |
KANBANTE_OBSIGATE_MCP_URL / _TOKEN / _TOOL / _ARGS |
(vide) | Serveur MCP d'ObsiGate : URL, jeton optionnel, outil de création de note, gabarit JSON d'arguments ({title}, {content}) |
KANBANTE_DIGEST_WEBHOOK_URL |
(vide) | Webhook générique recevant le digest en JSON {title, markdown, week} |
Sécurité
- Sans
KANBANTE_AUTH_*, quiconque atteint le port 8080 voit tous les dépôts visibles par le jeton et peut modifier les boards — l'app l'annonce par un avertissement au démarrage. Définis les identifiants, ou mets Kanbante derrière un reverse proxy authentifiant, et poseKANBANTE_REQUIRE_AUTH=1pour échouer fermé. - Tous les POST exigent un jeton CSRF (cookie double-submit posé par les
pages) : les appels API directs doivent d'abord charger une page pour
obtenir le cookie, puis envoyer le jeton (en-tête
X-CSRF-Tokenou champcsrf_token). - Aucun secret n'est versionné : le jeton passe uniquement par
l'environnement / le fichier
.envlocal (ignoré par git).
Architecture
kanbante/
├── app/
│ ├── main.py # FastAPI app (routes, auth, CSRF, lifespan)
│ ├── config.py # Réglages par variables d'environnement
│ ├── gitea_client.py # Client API Gitea asynchrone, paginé, avec cache
│ ├── models.py # SQLite (état du board local + migrations)
│ └── templates/ # Jinja2 (dashboard, board, focus, error)
├── static/
│ ├── css/style.css # Design system dark theme
│ ├── js/board.js # Drag & drop, filtres, création d'issues
│ └── vendor/ # HTMX, Alpine, SortableJS (vendorés, aucun CDN)
├── tests/ # Suite pytest
├── Dockerfile
├── docker-compose.yml
└── README.md
API
| Méthode | Route | Description |
|---|---|---|
| GET | / |
Dashboard tous les projets |
| GET | /board/{owner}/{repo} |
Kanban board |
| GET | /focus |
En cours + QA + En révision, tous projets actifs |
| GET | /wall |
Wallboard plein écran (rotation des projets actifs ; ?repo= pour un seul projet) |
| GET | /capture |
Page de capture éclair (formulaire + statut + instructions) |
| GET | /api/palette |
Données de la palette Ctrl+K (projets + recherche d'issues ?q=) |
| GET | /api/board/{owner}/{repo} |
Board en JSON + fragments HTML des cartes (base du board dynamique, ?milestone= accepté) |
| GET | /triage · /triage/{owner}/{repo} |
Mode Triage : sélecteur de projet puis session clavier sur le backlog |
| GET | /today |
Focus du jour : les 3 cartes épinglées, candidates, reprises d'hier |
| GET | /api/issues/{o}/{r}/{n}/code |
Branches, PRs et commits liés à la carte |
| GET | /standup · /api/standup |
Standup automatique (hier / aujourd'hui / blocages) |
| GET | /rules · POST /api/rules/run |
Règles d'automatisation : état, dernière évaluation, lancement manuel |
| POST | /api/issues/{o}/{r}/{n}/branch |
Créer la branche de la carte (issue-<n>-<slug> ou nom libre validé) et la passer en « En cours » |
| GET | /health · /health/{o}/{r} |
Santé du flux : goulots (temps médian par colonne) et cartes zombies, global ou par projet |
| POST | /api/today/pin · /api/today/unpin |
Épingler / retirer une carte du Focus du jour (max 3) |
| GET | /forecast |
Prévisions : débit hebdo, ETA backlog et milestones par projet |
| GET | /sprint/{owner}/{repo} · /api/sprint/{owner}/{repo} |
Récit de sprint d'un milestone (?milestone=), page rendue ou JSON markdown |
| POST | /api/issues/{o}/{r}/{n}/assign-self |
S'assigner une issue (utilisateur du jeton) |
| GET | /digest |
Digest hebdo : aperçu, statut des cibles, approbation d'envoi |
| POST | /digest/send |
Livrer le digest (cible obsigate ou webhook) — approbation explicite |
| POST | /api/capture |
Capture machine-à-machine (jeton X-Capture-Token, JSON ou texte : 1re ligne = titre) |
| GET | /activity |
Fil d'activité (journal d'événements, tous projets) |
| POST | /api/move |
Déplacer une issue (ordre persisté, synchro Gitea) |
| POST | /api/issues |
Créer une issue Gitea depuis le board |
| GET | /api/issues/{owner}/{repo}/{n} |
Détail d'une issue + commentaires (panneau) |
| POST | /api/issues/{owner}/{repo}/{n}/comments |
Commenter l'issue dans Gitea |
| GET | /api/events |
Derniers événements du journal (?repo=, ?limit=) |
| GET | /api/metrics |
Débit + temps de cycle moyen (?repo=, ?window=) |
| POST | /webhooks/gitea |
Réception des webhooks Gitea (HMAC si secret défini) |
| GET | /api/stream |
Flux SSE des événements Gitea (rafraîchissement live) |
| GET | /board/{o}/{r}?milestone= |
Board filtré par milestone (titre ou id) |
| GET | /search?q= |
Recherche globale d'issues (tous projets, tous états) |
| GET | /my |
Issues ouvertes assignées à l'utilisateur du jeton |
| GET | /briefing |
Briefing quotidien (page lisible + statut ntfy) |
| GET | /api/briefing |
Briefing en JSON + texte formaté |
| POST | /api/briefing/send |
Envoyer le briefing sur ntfy maintenant |
| GET/POST | /mcp |
Serveur MCP (JSON-RPC 2.0) : lectures + create_issue / add_comment |
| GET/POST | /api/repos/{o}/{r}/labels |
Lister / créer les étiquettes du dépôt |
| DELETE | /api/repos/{o}/{r}/labels/{id} |
Supprimer une étiquette du dépôt |
| POST | /api/issues/{o}/{r}/{n}/labels |
Ajouter / retirer une étiquette sur une issue |
Brancher un client MCP
Kanbante expose un serveur MCP : lectures d'abord, plus deux écritures minimales et sûres pour les agents — créer une carte et commenter (aucune suppression, aucun changement d'état). Configuration type (transport HTTP) :
{
"mcpServers": {
"kanbante": {
"url": "https://<votre-hôte-kanbante>/mcp"
}
}
}
Outils : list_projects, get_board, get_issue, get_focus,
search_issues, get_activity, get_metrics, create_issue
(owner, repo, title, body et column optionnels),
add_comment (owner, repo, number, body). Si
l'authentification Kanbante est active, le client doit envoyer
l'en-tête Authorization correspondant : Basic, ou un jeton
d'API personnel Bearer kbt_… (Mon profil → Jetons d'API) —
honoré uniquement sur /mcp et /api/, jamais sur le web.
Activer le temps réel (webhook Gitea)
Dans chaque dépôt (ou au niveau d'une organisation) : Réglages → Webhooks → Ajouter un webhook → Gitea, avec :
- URL cible :
https://<votre-hôte-kanbante>/webhooks/gitea - Type de contenu :
application/json - Secret : la même valeur que
KANBANTE_WEBHOOK_SECRET - Déclencheur : « Événements d'issue » (et « Commentaire d'issue » si vous voulez aussi le rafraîchissement sur commentaires externes)
Dès lors, fermer ou rouvrir une issue dans Gitea met à jour les boards
Kanbante ouverts, sans rechargement manuel.
| POST | /api/notes | Sauvegarder des notes |
| GET | /api/stats | Statistiques JSON |
| GET | /api/projects | Projets JSON (actifs / inactifs / archivés) |
| GET | /api/health | Liveness (aucune dépendance) |
| GET | /api/readiness | Readiness : teste SQLite + Gitea (503 si KO) |
Les POST répondent avec des codes explicites : 400 entrée invalide,
403 jeton CSRF manquant/invalide, 413 note trop longue, 502 Gitea
injoignable.
Tests
pip install -e ".[dev]"
pytest
La CI (.gitea/workflows/ci.yml) lance la même suite à chaque push.
Licence
MIT — voir LICENSE. Historique des versions : CHANGELOG.md.