diff --git a/.dockerignore b/.dockerignore
index e9f701b..2b12b55 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -15,6 +15,7 @@ frontend/.angular
backend/SchotzIadKmTracker.Tests
# --- Bases SQLite locales (la base vit sur le volume /data) ---
+data/
**/*.db
**/*.db-shm
**/*.db-wal
diff --git a/.gitignore b/.gitignore
index 6916aeb..d48aec5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,8 @@ backend/**/obj/
# --- Bases de données SQLite (données locales, non versionnées) ---
*.db
+# Dossier de la base SQLite (bind mount Docker) — ne jamais versionner
+/data/
*.db-shm
*.db-wal
diff --git a/.kiro/specs/schotziad-km-tracker/design.md b/.kiro/specs/schotziad-km-tracker/design.md
index f3f4453..46c67ba 100644
--- a/.kiro/specs/schotziad-km-tracker/design.md
+++ b/.kiro/specs/schotziad-km-tracker/design.md
@@ -44,7 +44,7 @@
- **Service externe — adresses** : API BAN (Base Adresses Nationale) — `https://api-adresse.data.gouv.fr` — autocomplétion et géocodage des adresses françaises, sans clé d'authentification
- **Service externe — itinéraires** : OpenRouteService (ORS) — calcul de distance routière et géométrie d'itinéraire entre coordonnées géographiques connues
- **Génération PDF** : côté backend, bibliothèque QuestPDF
-- **Hébergement** : l'application est packagée et hébergée via **Docker Compose** — un conteneur `frontend` (Angular servi par nginx en interne) publié sur le port hôte **2202**, et un conteneur `backend` (ASP.NET Core API) non exposé à l'hôte. Le fichier SQLite est persisté sur un volume monté dans le conteneur backend. La terminaison **HTTPS est assurée par un reverse-proxy Caddy externe** (sur une autre machine) qui relaie vers `IP_hôte:2202` ; le backend ne fait donc pas de redirection HTTPS lui-même
+- **Hébergement** : l'application est packagée et hébergée via **Docker Compose** — un conteneur `frontend` (Angular servi par nginx en interne) publié sur le port hôte **2202**, et un conteneur `backend` (ASP.NET Core API) non exposé à l'hôte. Le fichier SQLite est persisté via un **bind mount** (`./data:/data`) dans le conteneur backend, ce qui rend `data/trajets.db` directement accessible sur l'hôte (inspection et sauvegarde sans entrer dans le conteneur). La terminaison **HTTPS est assurée par un reverse-proxy Caddy externe** (sur une autre machine) qui relaie vers `IP_hôte:2202` ; le backend ne fait donc pas de redirection HTTPS lui-même
---
@@ -121,7 +121,7 @@ sequenceDiagram
> **Justification** : L'application est mono-utilisateur et n'a pas vocation à être multi-instances. SQLite élimine la complexité d'installation d'un serveur de base de données, offre des transactions ACID complètes via Entity Framework Core, et est parfaitement supporté par EF Core avec les migrations standard. Le fichier `trajets.db` est autoportant et facilement sauvegardable.
>
-> **Emplacement en conteneur** : dans le déploiement Docker Compose, le fichier de base de données est stocké sous le chemin du volume monté `/data/trajets.db` (voir la section « Déploiement (Docker Compose) »). Le chemin est fourni au backend via la chaîne de connexion / variable d'environnement (`ConnectionStrings__Default=Data Source=/data/trajets.db`), ce qui garantit la persistance des données indépendamment du cycle de vie du conteneur.
+> **Emplacement en conteneur** : dans le déploiement Docker Compose, le fichier de base de données est stocké sous `/data/trajets.db`, monté via un **bind mount** depuis `./data` de l'hôte (voir la section « Déploiement (Docker Compose) » et « Accès à la base de données »). Le chemin est fourni au backend via la chaîne de connexion / variable d'environnement (`ConnectionStrings__Default=Data Source=/data/trajets.db`), ce qui garantit la persistance des données indépendamment du cycle de vie du conteneur.
### Décision d'architecture : BAN pour le géocodage, ORS pour l'itinéraire
@@ -1554,7 +1554,7 @@ La génération PDF est enveloppée dans un `CancellationToken` avec délai de 1
La solution est hébergée via **Docker Compose** avec deux services :
-- **`backend`** : API ASP.NET Core (.NET 8), exposant le port `8080` en interne. Le fichier SQLite est stocké sous le chemin du volume monté `/data/trajets.db`.
+- **`backend`** : API ASP.NET Core (.NET 8), exposant le port `8080` en interne. Le fichier SQLite est stocké sous `/data/trajets.db`, monté depuis `./data` de l'hôte via un bind mount (accès direct au fichier).
- **`frontend`** : application Angular compilée et servie par **nginx**. Nginx sert la SPA (fallback vers `index.html` pour le routage côté client) et agit comme reverse-proxy en relayant les requêtes `/api` vers le service `backend`. C'est le seul service exposé à l'hôte (port **`2202`** de l'hôte → nginx). La terminaison HTTPS est assurée par un reverse-proxy **Caddy externe**.
```mermaid
@@ -1564,7 +1564,7 @@ graph LR
NGINX[frontend
nginx + SPA Angular]
API[backend
ASP.NET Core :8080]
NGINX -- "proxy /api → backend:8080" --> API
- API -- "/data/trajets.db" --> VOL[(Volume nommé
trajets-data)]
+ API -- "/data/trajets.db" --> VOL[(Bind mount
./data sur l'hôte)]
end
```
@@ -1664,7 +1664,8 @@ services:
# URL publique du frontend (liens de réinitialisation de mot de passe), ex. https://mon-domaine
- Auth__FrontendBaseUrl=${AUTH_FRONTEND_BASE_URL}
volumes:
- - trajets-data:/data
+ # Bind mount : base accessible sur l'hôte dans ./data/trajets.db
+ - ./data:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
@@ -1683,9 +1684,6 @@ services:
backend:
condition: service_healthy
restart: unless-stopped
-
-volumes:
- trajets-data: # persistance SQLite au-delà du cycle de vie des conteneurs
```
> **Endpoint de santé** : le backend expose `GET /health` (via `AddHealthChecks()` / `MapHealthChecks("/health")`) utilisé par le `healthcheck` Compose et la condition `depends_on: service_healthy`.
@@ -1697,7 +1695,7 @@ volumes:
- L'**URL publique du frontend** (`AUTH_FRONTEND_BASE_URL`, ex. `https://mon-domaine`) sert à construire les liens de réinitialisation de mot de passe envoyés par e-mail — et non le port interne.
- La **configuration e-mail** (SMTP via MailKit) est optionnelle : si elle est absente, le lien de réinitialisation est journalisé en repli (fallback) au lieu d'être envoyé.
- L'**API BAN** ne nécessite aucune clé — aucune configuration de secret n'est requise pour le géocodage.
-- Le **volume nommé `trajets-data`** (monté sur `/data`) garantit que le fichier `trajets.db` persiste à travers les redémarrages, recréations et mises à jour des conteneurs.
+- Le **bind mount `./data:/data`** garantit que le fichier `trajets.db` persiste à travers les redémarrages, recréations et mises à jour des conteneurs, tout en le rendant directement accessible sur l'hôte.
### Migrations au démarrage
@@ -1705,6 +1703,41 @@ Les **migrations EF Core sont appliquées automatiquement au démarrage du backe
> **Documents de spécification embarqués** : le `Dockerfile` backend a pour contexte la racine du dépôt et copie `.kiro/specs/schotziad-km-tracker/{requirements,design,tasks}.md` dans `./specs/` de l'image. Le `SpecDocumentService` lit en priorité ce dossier embarqué (`ContentRootPath/specs`), avec repli sur `.kiro/specs/...` en développement. L'application est ainsi autonome, sans montage du dépôt à l'exécution.
+### Accès à la base de données
+
+La base SQLite est exposée à l'hôte via un **bind mount** (`./data:/data` dans `docker-compose.yml`) : le fichier `data/trajets.db` est directement lisible et sauvegardable **sans entrer dans le conteneur**, à côté du `docker-compose.yml`.
+
+**Inspection / lecture** : ouvrir `data/trajets.db` avec un outil SQLite (DB Browser for SQLite, `sqlite3`, extension VS Code…). La lecture pendant que l'application tourne est sûre.
+
+**Sauvegarde à froid (recommandée)** : SQLite tolère mal l'écriture concurrente. Pour un backup cohérent, arrêter le backend le temps de la copie, ou utiliser la commande de sauvegarde en ligne intégrée :
+
+```bash
+# Option 1 — snapshot à froid (cohérence garantie)
+docker compose stop backend
+cp data/trajets.db "backup-$(date +%F).db"
+docker compose start backend
+
+# Option 2 — sauvegarde en ligne SQLite (sans arrêt), si sqlite3 est installé sur l'hôte
+sqlite3 data/trajets.db ".backup 'backup-$(date +%F).db'"
+```
+
+> **Concurrence** : éviter d'écrire dans `data/trajets.db` depuis l'extérieur pendant que l'application tourne (risque de corruption). La lecture reste sûre.
+
+**Migration depuis l'ancien volume nommé** : les déploiements antérieurs utilisaient un volume nommé Docker (`trajets-data`). Pour récupérer ces données dans le nouveau bind mount `./data` avant de basculer :
+
+```bash
+# Extraire la base de l'ancien volume vers ./data (backend encore basé sur l'ancienne image)
+mkdir -p data
+docker compose cp backend:/data/trajets.db ./data/trajets.db # si le conteneur tourne encore
+# — ou, si le service est déjà recréé, copier depuis le volume nommé :
+docker run --rm -v trajets-data:/from -v "$PWD/data":/to alpine \
+ sh -c "cp -a /from/. /to/"
+# Puis recréer avec le bind mount :
+docker compose up -d --build
+```
+
+Après migration et vérification, l'ancien volume nommé peut être supprimé : `docker volume rm trajets-data`.
+
> **Migrations notables** : `VehiculeDenomination` (dénomination du véhicule), `TrajetGeometrieJson` (géométrie d'itinéraire portée par le trajet), `TrancheBaremeKmMaxNullable` / bornes exclusives / coefficient numérique (barème multi-tranches à dernière tranche illimitée), `AjoutAuthentification` (comptes + jetons de réinitialisation), `TrajetAllerRetour` (indicateur aller/retour).
## Error Handling
diff --git a/docker-compose.yml b/docker-compose.yml
index f416137..55a0b66 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -22,7 +22,10 @@ services:
- Smtp__Password=${SMTP_PASSWORD:-}
- Smtp__From=${SMTP_FROM:-}
volumes:
- - trajets-data:/data
+ # Bind mount : la base SQLite est directement accessible sur l'hôte dans ./data/trajets.db,
+ # sans entrer dans le conteneur (inspection, sauvegarde). Le dossier ./data est créé au
+ # premier démarrage s'il n'existe pas.
+ - ./data:/data
restart: unless-stopped
# Healthcheck sur GET /health. curl est installé dans l'image backend (voir Dockerfile).
healthcheck:
@@ -42,6 +45,3 @@ services:
backend:
condition: service_healthy
restart: unless-stopped
-
-volumes:
- trajets-data: # persistance SQLite au-delà du cycle de vie des conteneurs
diff --git a/run-docker.sh b/run-docker.sh
deleted file mode 100755
index 8245810..0000000
--- a/run-docker.sh
+++ /dev/null
@@ -1,92 +0,0 @@
-#!/usr/bin/env bash
-# Lancement de SchotzIAD KM Tracker SANS docker compose (topologie équivalente).
-# Utile quand docker compose est indisponible/cassé.
-#
-# Usage :
-# ./run-docker.sh up # build + démarre backend et frontend
-# ./run-docker.sh down # arrête et supprime les conteneurs (garde le volume/les données)
-# ./run-docker.sh destroy # arrête tout ET supprime le volume SQLite (perte des données)
-# ./run-docker.sh logs # affiche les logs des deux conteneurs
-#
-# Selon votre config Docker, préfixez par sudo : sudo ./run-docker.sh up
-set -euo pipefail
-
-# Détecte s'il faut sudo (utilisateur hors du groupe docker).
-DOCKER="docker"
-if ! docker info >/dev/null 2>&1; then
- if sudo -n docker info >/dev/null 2>&1 || sudo docker info >/dev/null 2>&1; then
- DOCKER="sudo docker"
- echo "[info] Docker requiert sudo — utilisation de 'sudo docker'."
- else
- echo "[erreur] Démon Docker inaccessible (ni direct, ni via sudo)." >&2
- exit 1
- fi
-fi
-
-ROOT="$(cd "$(dirname "$0")" && pwd)"
-NET="schotziad-net"
-VOL="trajets-data"
-ORS_KEY="${ORS_API_KEY:-}"
-
-# Charge ORS_API_KEY depuis .env s'il existe et n'est pas déjà défini.
-if [[ -z "$ORS_KEY" && -f "$ROOT/.env" ]]; then
- ORS_KEY="$(grep -E '^ORS_API_KEY=' "$ROOT/.env" | head -n1 | cut -d= -f2- || true)"
-fi
-
-up() {
- echo "[1/5] Réseau et volume…"
- $DOCKER network inspect "$NET" >/dev/null 2>&1 || $DOCKER network create "$NET"
- $DOCKER volume inspect "$VOL" >/dev/null 2>&1 || $DOCKER volume create "$VOL"
-
- echo "[2/5] Build backend…"
- $DOCKER build -t schotziad-backend "$ROOT/backend"
-
- echo "[3/5] Build frontend…"
- $DOCKER build -t schotziad-frontend "$ROOT/frontend"
-
- echo "[4/5] Démarrage backend (nom d'hôte réseau: backend)…"
- $DOCKER rm -f schotziad-backend >/dev/null 2>&1 || true
- $DOCKER run -d --name schotziad-backend --network "$NET" --network-alias backend \
- -e "ASPNETCORE_URLS=http://+:8080" \
- -e "ConnectionStrings__Default=Data Source=/data/trajets.db" \
- -e "ORS__ApiKey=${ORS_KEY}" \
- -v "$VOL:/data" \
- --restart unless-stopped \
- schotziad-backend
-
- echo "[5/5] Démarrage frontend (http://localhost:8080)…"
- $DOCKER rm -f schotziad-frontend >/dev/null 2>&1 || true
- $DOCKER run -d --name schotziad-frontend --network "$NET" \
- -p 8080:80 \
- --restart unless-stopped \
- schotziad-frontend
-
- echo
- echo "Prêt. Ouvrez http://localhost:8080"
- echo "Logs : ./run-docker.sh logs | Arrêt : ./run-docker.sh down"
-}
-
-down() {
- $DOCKER rm -f schotziad-frontend schotziad-backend >/dev/null 2>&1 || true
- echo "Conteneurs arrêtés (le volume '$VOL' et les données sont conservés)."
-}
-
-destroy() {
- down
- $DOCKER volume rm "$VOL" >/dev/null 2>&1 || true
- $DOCKER network rm "$NET" >/dev/null 2>&1 || true
- echo "Volume, réseau et données supprimés."
-}
-
-logs() {
- echo "=== backend ==="; $DOCKER logs --tail 40 schotziad-backend 2>&1 || true
- echo "=== frontend ==="; $DOCKER logs --tail 20 schotziad-frontend 2>&1 || true
-}
-
-case "${1:-up}" in
- up) up ;;
- down) down ;;
- destroy) destroy ;;
- logs) logs ;;
- *) echo "Usage: $0 {up|down|destroy|logs}" >&2; exit 2 ;;
-esac