# 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/depot` fige 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 `#n` sur la carte (badge `⑂`) et dans le panneau détail (section « Code lié ») - 🔀 **PR fusionnée → Terminé** — le webhook `pull_request` retrouve les issues liées (références `#n`, branche `issue-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--` 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é depuis `KANBANTE_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-5` colonnes, `L` étiquettes, `A` s'assigner, `X` fermer, `Entrée` suivante, `Z` annuler), 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), endpoint `POST /api/capture` proté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 (source `gitea`). Une seule étiquette `kanban:*` 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 `/briefing` et 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 `/mcp` pour 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 ```bash # 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é) ```bash 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 ```bash 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) ```bash 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_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 pose `KANBANTE_REQUIRE_AUTH=1` pour é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-Token` ou champ `csrf_token`). - Aucun secret n'est versionné : le jeton passe uniquement par l'environnement / le fichier `.env` local (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--` 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) : ```json { "mcpServers": { "kanbante": { "url": "https:///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:///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 ```bash pip install -e ".[dev]" pytest ``` La CI (`.gitea/workflows/ci.yml`) lance la même suite à chaque push. ## Licence MIT — voir [LICENSE](LICENSE). Historique des versions : [CHANGELOG.md](CHANGELOG.md).