Table today_picks; POST /api/today/pin (max 3, idempotent) et
/unpin; page /today (picks en direct, candidates de l'encours,
reprises d'hier, récap du jour depuis le journal); bouton 📌 sur
les cartes (délégation capture dans today.js, fragments live
compris); sections dédiées dans le briefing du matin.
←/→ et contrôles à l'écran changent de projet; navigation manuelle
= pause de rotation 90 s; Espace = pause franche avec badge. La
position survit aux rechargements via sessionStorage (le cycle de
12 projets x 30 s dépassait le refresh de sécurité de 5 min: le
mur semblait figé). Fix: l'écoute SSE utilisait onmessage, inerte
pour les événements nommés 'gitea' du hub.
/triage (sélecteur par taille de backlog) + /triage/{o}/{r}
(session plein cadre, file embarquée): 1-5 colonnes, X fermer,
L étiquettes, A s'assigner (nouvel endpoint assign-self +
client.set_issue_assignees, idempotent), Entrée suivante, Z annuler.
Progression + résumé de session; décisions via /api/move et les
API existantes. Entrées: board + palette.
Diffusion SSE sur toutes les mutations (move, création, capture,
étiquettes) via _publish_board_changed; GET /api/board/{o}/{r}
(fragments _card.html rendus serveur, même pipeline que la page);
board-live.js: diff en place par colonne avec animation FLIP,
application différée pendant un drag, filtres/compteurs réappliqués,
repli par sondage 45 s sans webhooks. Carte extraite en partiel
_card.html partagé page/API.
POST /api/capture (X-Capture-Token ou Bearer; 503 si non configuré;
exempté Basic Auth/CSRF car machine-à-machine), JSON ou texte libre
(1re ligne = titre), dépôt par défaut KANBANTE_CAPTURE_REPO.
Module app/capture.py: parsing partagé + abonné ntfy en tâche de
fond (reconnexion, curseur kv_store anti-rejeu, tag 'kanban'...
'kanbante' ignoré). _do_capture commun: issue créée, carte placée,
événement 'created' source 'capture'.
Rotation automatique entre projets actifs (30 s), compteurs des
six colonnes, cartes en vol avec âge, fermées du jour (journal),
horloge; rafraîchissement en direct par le flux SSE (rechargement,
aucune interaction à protéger) + filet de sécurité à 5 min.
?repo= fige un projet. Lien de navigation ajouté partout.
- KANBAN_COLUMNS gagne 'qa' (🧪, #06b6d4, étiquette kanban:qa);
Focus inclut QA; dashboard et board affichent les 6 colonnes.
- _adopt_label_columns: une étiquette kanban:* unique qui diverge
de l'état local fait adopter sa colonne (board + focus), avec
événement journalisé source 'gitea'. C'est ce qui permet de
piloter le board depuis Gitea même.
Rendu par le serveur pour les colonnes vides au chargement, il
n'était jamais retiré après un dépôt (signalé par Bruno). Nouvelle
fonction refreshEmptyHints() appelée après chaque déplacement et
après chaque retour arrière: indice retiré si la colonne contient
une carte, rétabli si elle devient vide. SortableJS restreint aux
cartes (draggable: '.kanban-card'). Logique vérifiée par mini-test
DOM (4 scénarios) en plus de node --check.
Le faux omettait la clé pull_request sur les issues, ce qui a
masqué le bug de la 0.5.1 pendant que 84 tests restaient verts.
Clé désormais toujours présente (null = issue, objet = PR) +
4 tests dédiés dans test_regression_gitea_payload.py.
Gitea 1.27.3 renvoie "pull_request": null sur les issues ordinaires:
_is_issue testait la présence de la clé et classait chaque vraie
issue comme un pull request. Board, Focus et briefing affichaient
zéro carte contre un vrai Gitea (le dashboard, basé sur les
compteurs de dépôts, semblait normal, d'où le signalement
« la nouvelle issue n'apparaît pas »). Le filtre teste maintenant
la valeur. Vérifié en réel: board bruno/flowdeck 0 -> 6 cartes.
- modale Étiquettes: liste, création (nom + couleur + palette),
suppression via DELETE (CSRF par en-tête); panneau détail:
bascules d'étiquettes par issue avec mise à jour immédiate des
chips de la carte; _issue_payload expose les ids d'étiquettes
- API étiquettes dans main (routes repos + toggle issue)
- notes: bouton replier/déplier et largeur par glisser, persistés
par projet (localStorage), sauvegarde automatique débouncée
avec statut, formulaire repris par board.js (HTMX retiré du board)
- nouvelles pages search/my/briefing; navigation enrichie partout
(Mes issues, Briefing, Recherche); CSS associé
- client: search_issues (/repos/issues/search, type=issues,
assigned_by), delete_label + forget_label_cache; models: kv_store
- /search groupe par projet avec colonne Kanban; /my = issues
ouvertes assignées à l'utilisateur du jeton (get_user)
- briefing: _briefing_data (focus, âgées, milestones <= 7 j,
activité journal), page /briefing, /api/briefing, envoi ntfy
manuel + planificateur lifespan à KANBANTE_BRIEFING_TIME
(kv briefing_last_sent, pas de doublon); app/briefing.py =
format texte + envoi ntfy (en-têtes ASCII)
- MCP: POST /mcp JSON-RPC (initialize, tools/list, tools/call,
ping, notifications 202), 7 outils en lecture seule, GET /mcp
de découverte; erreurs -32601/-32700, isError pour les outils
- focus refactoré en _collect_focus_groups réutilisé par le
briefing et l'outil MCP get_focus
FakeGitea: labels (repo_labels/label_ops), milestones (Sprint 1 sur
issues 1 et 3), invalidate_cache compteur. Le flux SSE est testé au
niveau du générateur: les clients de test HTTP ne ferment pas
proprement un flux infini (vérifié empiriquement); le bout-en-bout
HTTP a été validé en smoke test réel (curl + webhook).
- en-tête vitré sticky, navigation en pastilles avec état actif,
logo en tuile dégradée, fil d'Ariane projet
- dashboard: hero + tuiles de synthèse (actifs, fermées/déplacements
30 j, cycle moyen), barre de progression par projet, états vides
guidés, raccourci clavier /
- board: sélecteur de milestones + bandeau de progression, colonnes
à défilement indépendant, placeholders de colonnes vides, hint
sur le panneau de notes, toast et drawer animés
- live.js: recharge douce conditionnelle (visible, pas de drawer,
pas de saisie, pas de drag) sinon pastille d'actualisation
- accessibilité: anneau de focus visible, prefers-reduced-motion,
responsive retravaillé; board.js: drapeau de drag partagé avec
live.js + raccourci / vers le filtre
- POST /webhooks/gitea: HMAC-SHA256 si KANBANTE_WEBHOOK_SECRET,
applique fermetures/réouvertures externes de façon idempotente
(l'écho de nos propres appels est un no-op, journal source=webhook),
invalide le cache, diffuse sur le hub SSE
- GET /api/stream (SSE) + LiveHub; générateur testable séparément
- synchro d'étiquettes kanban:* à chaque déplacement (ensure/add/
remove granulaires, étiquettes utilisateur intactes, flag
KANBANTE_SYNC_LABELS, échec rapporté label_sync sans casser le move)
- board ?milestone=<titre|id>: filtre, échéance, progression;
client: get_milestones, labels, invalidate_cache public
- dashboard: métriques 30 j du journal exposées au template
FakeGitea étendu (get_issue, commentaires); le test de réconciliation
suit le nouveau retour de reconcile_board_state; l'assertion de
version suit APP_VERSION.
- drawer de détail: corps markdown rendu (marked + DOMPurify
vendorés), commentaires, formulaire de commentaire vers Gitea;
clic distingué du drag par la distance du pointeur
- badges ⏳ N j au-delà du seuil d'âge (rouge au double)
- en-têtes de colonnes: n/limite, classe .over en rouge, toast
d'avertissement au dépôt dans une colonne saturée
- page activity.html, liens Activité dans dashboard et board
- board_events append-only + log_event/get_events/column_entry_times/
get_metrics (débit, temps de cycle); journalisation des moves,
créations, fermetures/rouvertures (UI et réconciliation Gitea),
commentaires; un reclassement intra-colonne ne journalise pas
- client Gitea: get_issue, get_issue_comments (paginés), create_comment
- API: GET /api/issues/{o}/{r}/{n} (404 propre), POST .../comments,
GET /api/events, GET /api/metrics, page /activity
- config: KANBANTE_WIP_LIMITS (colonne:limite) et KANBANTE_AGING_DAYS;
le board calcule le temps en colonne (journal, replis updated_at
puis création de l'issue)
- reconcile_board_state retourne aussi les numéros fermés/purgés
from tests.conftest import ... levait ModuleNotFoundError en CI :
le binaire pytest n'ajoute pas le répertoire courant à sys.path,
contrairement à python -m pytest (utilisé en local, qui masquait le
problème). Correctifs : tests/__init__.py (package), pythonpath=[.]
dans la config pytest, et étape CI en python -m pytest.
Vérifié : 36/36 dans les trois modes d'invocation.
- vendor: HTMX/Alpine/SortableJS servis depuis /static/vendor (M3)
- board.js: jeton CSRF, ordres source/destination envoyés, carte
replacée + toast rouge en cas d'échec (M8), filtres texte/étiquette,
formulaire de création d'issue
- dashboard: 3 onglets Actifs/Inactifs/Archivés, recherche via
data-search échappé au lieu d'une interpolation JS (M9)
- board: cartes liées vers l'issue Gitea, méta CSRF, notes protégées
- nouvelles pages focus.html et error.html
- CSS: fond des badges en color-mix, l'ancien var(--accent)20 était
invalide (M1); styles toolbar/focus/erreur/toast erreur
- Dashboard with all Gitea projects (active/archived)
- Kanban board per project (Backlog → À faire → En cours → Révision → Terminé)
- Drag & drop issues between columns (SortableJS)
- Project notes panel
- Gitea API integration (read issues, repos)
- SQLite local state for board positions
- Dark theme design system
- Docker-ready (Dockerfile + docker-compose.yml)
- FastAPI + Jinja2 + HTMX + Alpine.js stack