chore(release): Kanbante 0.2.0 — Docker, docs, licence
CI / test (push) Failing after 1m37s

- Dockerfile: copie du code avant installation (le package installé
  n'est plus vide — I6), utilisateur non-root, HEALTHCHECK
- compose: version: obsolète retiré, variables d'auth, healthcheck
- pyproject: version 0.2.0, jinja2 <4, config pytest
- LICENSE (MIT), CHANGELOG 0.2.0, README réécrit (configuration,
  sécurité, API, tests), .dockerignore
This commit is contained in:
Spark
2026-10-11 09:31:48 -04:00
parent 5aa0c7d9a7
commit c7212e88ad
8 changed files with 220 additions and 35 deletions
+13
View File
@@ -0,0 +1,13 @@
.git
.gitignore
.venv
__pycache__
*.pyc
.pytest_cache
*.egg-info
.env
/data/
.kanbante/
tests/
README.md
CHANGELOG.md
+1
View File
@@ -1,5 +1,6 @@
__pycache__/
*.pyc
.pytest_cache/
.venv/
.env
*.egg-info/
+73
View File
@@ -0,0 +1,73 @@
# Changelog
## 0.2.0 — 2026-10-11
Consolidation complète à partir d'un audit du code 0.1.0 (chaque correctif
marqué « confirmé » avait été reproduit par un test avant correction).
### Critiques corrigés
- **Perte de données en Docker** : `KANBANTE_DB_PATH` est enfin lue par le
code — la base SQLite atterrit dans le volume `/data` comme prévu.
- **Authentification** : Basic Auth optionnelle mais recommandée
(`KANBANTE_AUTH_USER` / `KANBANTE_AUTH_PASSWORD`, `KANBANTE_REQUIRE_AUTH=1`
pour refuser de servir sans), protection CSRF (double-submit) sur tous
les POST, avertissement bruyant au démarrage quand l'auth est absente.
- **Synchro bidirectionnelle** : déplacer une carte vers « Terminé » ferme
l'issue dans Gitea, la ressortir la rouvre (`KANBANTE_SYNC_GITEA=0` pour
désactiver). L'état local est réconcilié à chaque chargement de board
(fermées → Terminé, supprimées → purgées).
### Importants corrigés
- Classification des projets : fenêtre d'activité glissante configurable
(`KANBANTE_ACTIVE_WINDOW_DAYS`, défaut 365) et vrai drapeau `archived`
de Gitea — fini l'année codée en dur et l'onglet « Archivés » mensonger.
- Pagination complète des appels Gitea (le plafond serveur de 50 réponses
par page ne tronque plus les dépôts ni les issues, en silence).
- `get_board_state` ne plante plus sur une colonne inconnue ; migration
automatique des bases 0.1.0 (colonne `position`, `notes.updated_at`,
nettoyage des lignes invalides).
- Gitea injoignable → page d'erreur 502 propre (JSON pour l'API) au lieu
d'un 500 brut ; `/api/readiness` teste réellement SQLite et Gitea,
`/api/health` reste un liveness sans dépendance.
- Dockerfile : installation après copie du code (le package installé
n'est plus vide), utilisateur non-root, `HEALTHCHECK`, `.dockerignore`.
### Ajouts
- **Ordre des cartes persisté** dans chaque colonne (aller-retour
glisser-déposer → rechargement).
- **Création d'issues** depuis le board (`POST /api/issues`).
- **Vue Focus** (`/focus`) : tout ce qui est En cours / En révision,
tous projets actifs confondus.
- Filtres par texte et par étiquette sur le board ; les cartes lient
vers l'issue Gitea ; les pull requests ne sont plus affichées comme
des issues.
- Suite de tests `pytest` (modèles, client Gitea, API, auth/CSRF) et
CI Gitea Actions (`.gitea/workflows/ci.yml`).
- Fichier `LICENSE` (MIT, annoncé par le README depuis la 0.1.0).
### Détails
- Client Gitea entièrement asynchrone (les routes ne bloquent plus la
boucle d'événements), cache mémoire court des listes, en-tête
`Authorization` seulement si un jeton est configuré, uid résolu via
`/user` (plus de `uid=1` ni d'utilisateur codés en dur), `.env` chargé
depuis la racine du projet seulement.
- Bibliothèques JS (HTMX, Alpine, SortableJS) **vendorées** dans
`static/vendor/` : plus aucune dépendance CDN à l'exécution.
- Glisser-déposer : en cas d'échec serveur, la carte est replacée et le
toast d'erreur est rouge ; les déplacements envoient l'ordre complet
des colonnes source et destination.
- API : codes HTTP corrects (400 colonne invalide, 403 CSRF, 413 note
trop longue — limite configurable), `lifespan` FastAPI à la place de
`on_event` déprécié, imports nettoyés.
- CSS : fond des badges corrigé (`color-mix()` remplace l'invalide
`var(--accent)20`).
- Recherche du dashboard : le nom des dépôts passe par un attribut
`data-search` échappé, plus aucune interpolation dans du JS.
- `docker-compose.yml` : clé `version:` obsolète retirée, variables
d'auth exposées, healthcheck.
## 0.1.0 — 2026-07-07
Version initiale : dashboard des projets Gitea, board Kanban 5 colonnes
par dépôt (glisser-déposer SortableJS), notes par projet, état local
SQLite, thème sombre, Dockerfile + docker-compose.
+16 -8
View File
@@ -2,19 +2,27 @@ FROM python:3.12-slim
WORKDIR /app
# Install dependencies
COPY pyproject.toml .
RUN pip install --no-cache-dir .
ENV PYTHONUNBUFFERED=1 \
KANBANTE_DB_PATH=/data/kanbante.db \
GITEA_URL=https://git.dracodev.net
# Copy the package BEFORE installing: with the 0.1.0 layout (install
# first, copy app/ afterwards) the installed distribution was empty and
# the app only worked by accident of the working directory.
COPY pyproject.toml ./
COPY app/ ./app/
COPY static/ ./static/
RUN pip install --no-cache-dir .
# Create data directory
RUN mkdir -p /data
ENV KANBANTE_DB_PATH=/data/kanbante.db
ENV GITEA_URL=https://git.dracodev.net
# Persistent data directory (volume target) + non-root runtime user
RUN mkdir -p /data \
&& adduser --disabled-password --gecos "" appuser \
&& chown -R appuser:appuser /data /app
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/api/health', timeout=4)"
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Bruno Charest
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+81 -23
View File
@@ -4,13 +4,17 @@
## Features
- 📋 **Dashboard** — tous tes projets Gitea en un coup d'œil (actifs / archivés)
- 🗂️ **Kanban Board** — colonnes Backlog → À faire → En cours → Révision → Terminé
- 🎯 **Drag & Drop** — glisse les issues entre colonnes
- 🔗 **Gitea Sync** — les issues Gitea deviennent des cartes Kanban
- 📋 **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 → 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
- 🎯 **Vue Focus** — tout ce qui est En cours / En révision, tous projets confondus (`/focus`)
- 📝 **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, déploiement en 30 secondes
- 🐳 **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
@@ -22,6 +26,9 @@ 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
@@ -31,29 +38,34 @@ Ouvrir http://localhost:8080
## Docker
### Avec docker-compose (recommandé si docker-compose ≥ v2)
### Avec docker-compose (recommandé)
```bash
echo "GITEA_TOKEN=ton_token" > .env
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
# Builder l'image
docker build -t kanbante:latest .
# Créer le volume persistant
docker volume create kanbante_data
# Lancer
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
```
@@ -61,26 +73,55 @@ docker run -d \
### Déploiement sur Deepin 25 (Debian Bookworm)
```bash
# Installer Docker
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 la Basic Auth dès que les deux sont définis |
| `KANBANTE_REQUIRE_AUTH` | `0` | `1` = l'app refuse de servir (503) si l'auth n'est pas configurée |
| `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 |
## 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 + API)
│ ├── gitea_client.py # Client API Gitea
│ ├── models.py # SQLite (état du board local)
│ └── templates/ # Jinja2 (dashboard.html, board.html)
│ ├── 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 (SortableJS)
│ ├── 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
@@ -92,12 +133,29 @@ kanbante/
|---------|-------|-------------|
| GET | `/` | Dashboard tous les projets |
| GET | `/board/{owner}/{repo}` | Kanban board |
| POST | `/api/move` | Déplacer une issue |
| GET | `/focus` | En cours + En révision, tous projets actifs |
| POST | `/api/move` | Déplacer une issue (ordre persisté, synchro Gitea) |
| POST | `/api/issues` | Créer une issue Gitea depuis le board |
| POST | `/api/notes` | Sauvegarder des notes |
| GET | `/api/stats` | Statistiques JSON |
| GET | `/api/projects` | Projets JSON |
| GET | `/api/health` | Health check |
| 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
MIT — voir [LICENSE](LICENSE). Historique des versions :
[CHANGELOG.md](CHANGELOG.md).
+9 -2
View File
@@ -1,5 +1,3 @@
version: '3'
services:
kanbante:
build: .
@@ -10,9 +8,18 @@ services:
- GITEA_URL=${GITEA_URL:-https://git.dracodev.net}
- GITEA_TOKEN=${GITEA_TOKEN}
- KANBANTE_DB_PATH=/data/kanbante.db
# Strongly recommended: without these, the app is unauthenticated.
- KANBANTE_AUTH_USER=${KANBANTE_AUTH_USER:-}
- KANBANTE_AUTH_PASSWORD=${KANBANTE_AUTH_PASSWORD:-}
- KANBANTE_REQUIRE_AUTH=${KANBANTE_REQUIRE_AUTH:-0}
volumes:
- kanbante_data:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/api/health', timeout=4)"]
interval: 30s
timeout: 5s
start_period: 10s
volumes:
kanbante_data:
+6 -2
View File
@@ -1,12 +1,12 @@
[project]
name = "kanbante"
version = "0.1.0"
version = "0.2.0"
description = "Kanban project manager integrated with Gitea"
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.115",
"uvicorn[standard]>=0.32",
"jinja2>=3.1,<3.1.6",
"jinja2>=3.1,<4",
"httpx>=0.28",
"python-dotenv>=1.0",
"python-multipart>=0.0.18",
@@ -20,3 +20,7 @@ dev = [
"pytest>=8",
"pytest-asyncio>=0.24",
]
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"