From c7212e88ad1a99f019d6bd27d2de80dd20fd6c40 Mon Sep 17 00:00:00 2001 From: Spark Date: Sun, 11 Oct 2026 09:31:48 -0400 Subject: [PATCH] =?UTF-8?q?chore(release):=20Kanbante=200.2.0=20=E2=80=94?= =?UTF-8?q?=20Docker,=20docs,=20licence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .dockerignore | 13 ++++++ .gitignore | 1 + CHANGELOG.md | 73 +++++++++++++++++++++++++++++++ Dockerfile | 24 +++++++---- LICENSE | 21 +++++++++ README.md | 104 +++++++++++++++++++++++++++++++++++---------- docker-compose.yml | 11 ++++- pyproject.toml | 8 +++- 8 files changed, 220 insertions(+), 35 deletions(-) create mode 100644 .dockerignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bd5c3f4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +.git +.gitignore +.venv +__pycache__ +*.pyc +.pytest_cache +*.egg-info +.env +/data/ +.kanbante/ +tests/ +README.md +CHANGELOG.md diff --git a/.gitignore b/.gitignore index 3945315..c6ff227 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ __pycache__/ *.pyc +.pytest_cache/ .venv/ .env *.egg-info/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3adc17f --- /dev/null +++ b/CHANGELOG.md @@ -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. diff --git a/Dockerfile b/Dockerfile index 817ab46..ed23e5b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8cdeac5 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md index 009d155..8e71cab 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/docker-compose.yml b/docker-compose.yml index cb45430..4d92455 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/pyproject.toml b/pyproject.toml index 55a7539..0139f2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"