Le serveur MCP HTTP (token Bearer) se configure dans la config Claude Code locale (jamais dans le .mcp.json versionné). Ajout de la méthode cross-OS (claude mcp add), des emplacements de fichier par OS (Fedora/Linux, Windows, macOS) et de la procédure de régénération du token (invalidé au reseed BDD). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lesstime
Application de gestion de projet avec suivi du temps et portail client.
Stack
| Couche | Technologies |
|---|---|
| Backend | PHP 8.4, Symfony 8, API Platform 4, Doctrine ORM |
| Frontend | Nuxt 4 (SPA), Vue 3, Pinia, Tailwind CSS |
| Base de données | PostgreSQL 16 |
| Auth | JWT HTTP-only cookie (lexik/jwt-authentication-bundle) |
| Infrastructure | Docker (PHP-FPM, Nginx, PostgreSQL) |
Fonctionnalités
- Gestion de projets et tâches (kanban, groupes, priorités, tags, efforts)
- Suivi du temps (timer, calendrier, vue liste)
- Portail client avec tickets (bug, amélioration, autre)
- Gestion de documents (upload, prévisualisation, téléchargement)
- Profil utilisateur avec avatar (crop circulaire)
- Notifications temps réel
- Intégration Gitea (issues, repos)
- Intégration Mail IMAP (boîte partagée OVH, voir
docs/mail-integration.md) - Serveur MCP pour assistants IA
- Error tracking centralisé back + front (GlitchTip / SDK Sentry, prod uniquement — voir « Error tracking »)
- Multi-langue (i18n)
Prérequis
- Docker & Docker Compose
- Git
Installation
# 1. Cloner le repo
git clone <url> && cd lesstime
# 2. Démarrer les containers
make start
# 3. Installation complète (composer, migrations, fixtures, build Nuxt)
make install
L'application est accessible sur http://localhost:8082.
Les valeurs par défaut du .env committé suffisent pour démarrer en local. Pour la prod
(et pour activer la messagerie), surcharger les variables sensibles dans .env.local —
voir « Variables d'environnement » ci-dessous.
Comptes de test (fixtures)
| Utilisateur | Mot de passe | Rôle | Détails |
|---|---|---|---|
admin |
admin |
ROLE_ADMIN | Administrateur |
alice |
alice |
ROLE_USER | Utilisateur interne |
bob |
bob |
ROLE_USER | Utilisateur interne |
charlie |
charlie |
ROLE_USER | Utilisateur interne |
client-liot |
client |
ROLE_CLIENT | Client LIOT (projet SIRH) |
client-acme |
client |
ROLE_CLIENT | Client ACME (projet CRM) |
Variables d'environnement
Les variables sont définies dans .env (committé, valeurs par défaut pour le dev) et
peuvent être surchargées dans .env.local (jamais committé). En prod, elles vont dans le
.env du serveur (/var/www/lesstime/.env, voir infra/prod/.env.example).
| Variable | Rôle | Défaut dev | À fixer en prod |
|---|---|---|---|
APP_SECRET |
Secret Symfony | placeholder | ✅ (hex 32) |
JWT_PASSPHRASE |
Passphrase des clés JWT | placeholder | ✅ |
DATABASE_URL |
Connexion PostgreSQL | container db |
✅ (host.docker.internal) |
CORS_ALLOW_ORIGIN |
Origines CORS autorisées | localhost | ✅ (domaine prod) |
ENCRYPTION_KEY |
Clé hex 32 bytes chiffrant les credentials IMAP/SMTP (feature mail) | placeholder | ✅ — doit rester stable, sinon les credentials mail stockés deviennent illisibles |
LOCK_DSN |
Store de verrous Symfony pour la sync mail (anti-chevauchement) | flock |
flock suffit |
SENTRY_DSN |
Error tracking backend → GlitchTip (projet lesstime-api) |
(vide) | ⚪ optionnel — active le tracking (voir « Error tracking ») |
Messagerie :
ENCRYPTION_KEYetLOCK_DSNsont introduites par l'intégration mail. Détails de config et cron de synchronisation :docs/mail-integration.mdetdocs/mail-cron-setup.md. Générer une clé :php -r "echo bin2hex(random_bytes(32));".
Commandes
Docker
make start # Démarrer les containers
make stop # Arrêter les containers
make restart # Redémarrer les containers
make shell # Shell dans le container PHP
make shell-root # Shell root dans le container PHP
Développement
make dev-nuxt # Dev server Nuxt (hot reload, port 3002)
make cache-clear # Vider le cache Symfony
make logs-dev # Tail logs Symfony
make mail-sync # Synchroniser la boîte mail IMAP (voir docs/mail-cron-setup.md)
Base de données
make migration-migrate # Lancer les migrations
make fixtures # Charger les fixtures
make db-reset # Reset BDD + migrations + fixtures (⚠️ supprime les données)
Tests & Qualité
make test # PHPUnit
make php-cs-fixer-allow-risky # Fix code style PHP (Symfony + PSR-12)
Installation complète
make install # Composer + migrations + fixtures + build Nuxt
make reset # Tout supprimer et réinstaller (⚠️ supprime la BDD)
Architecture
src/
├── Entity/ # Entités Doctrine
├── ApiResource/ # Ressources API Platform (découplées)
├── State/ # Providers et Processors API Platform
├── Controller/ # Controllers custom Symfony
├── Service/ # Services métier
├── EventListener/ # Listeners Doctrine
├── Exception/ # Exceptions custom
├── Security/ # Authenticators custom
├── Repository/ # Repositories Doctrine
├── Command/ # Commandes console
├── DataFixtures/ # Fixtures
└── Mcp/Tool/ # MCP tools par domaine
├── Project/
├── Task/
├── TaskMeta/
├── TimeEntry/
└── Reference/
frontend/
├── pages/ # Pages Nuxt (routing auto)
│ ├── portal/ # Pages portail client
│ └── projects/ # Pages projets
├── layouts/ # Layouts (default, portal)
├── components/ # Composants Vue
│ ├── ui/ # Composants génériques
│ ├── task/ # Tâches
│ ├── user/ # Utilisateur (avatar, etc.)
│ ├── project/ # Projets
│ ├── client/ # Clients
│ ├── client-ticket/ # Tickets client
│ ├── admin/ # Administration
│ ├── notification/ # Notifications
│ └── time-tracking/ # Suivi du temps
├── composables/ # Composables (useApi, useNotifications, etc.)
├── stores/ # Stores Pinia (auth, ui, timer)
├── services/ # Services API
│ └── dto/ # Types TypeScript
├── plugins/ # Plugins Nuxt
├── utils/ # Utilitaires
├── i18n/locales/ # Traductions
└── middleware/ # Middleware auth
config/ # Config Symfony
migrations/ # Migrations Doctrine
docker/ # Dockerfiles et config Nginx
Docker
| Container | Port | Description |
|---|---|---|
php-lesstime-fpm |
3002 (dev Nuxt) | PHP-FPM + Node 24 |
nginx-lesstime |
8082 | Nginx reverse proxy |
| PostgreSQL | 5435 | Base de données |
Configuration : infra/dev/.env.docker (override local : infra/dev/.env.docker.local)
API
Toutes les routes API sont préfixées /api (API Platform).
- Documentation auto-générée : http://localhost:8082/api
- Auth :
POST /login_checkavec{ username, password }→ cookie JWTBEARER
Serveur MCP
Lesstime expose un serveur MCP (Model Context Protocol) permettant aux assistants IA d'interagir avec les données.
Tools disponibles (22)
| Domaine | Tools |
|---|---|
| Reference | list-users, list-clients |
| Project | list-projects, get-project, create-project, update-project |
| Task | list-tasks, get-task, create-task, update-task, delete-task |
| TaskMeta | list-statuses, list-priorities, list-efforts, list-tags, list-groups, create-group, update-group |
| TimeEntry | list-time-entries, create-time-entry, update-time-entry, delete-time-entry |
Configuration locale (STDIO)
{
"mcpServers": {
"lesstime": {
"command": "docker",
"args": ["exec", "-i", "php-lesstime-fpm", "php", "bin/console", "mcp:server"]
}
}
}
Configuration réseau (HTTP) — par poste, hors git
Le transport HTTP nécessite un token API (Bearer), qui est un secret : il ne va jamais
dans le .mcp.json versionné (celui-ci ne contient que le serveur STDIO local, sans secret).
Chaque développeur configure le serveur HTTP dans sa config Claude Code locale.
Méthode recommandée (identique sur Fedora, Windows et macOS) :
claude mcp add --transport http --scope user lesstime \
http://project.malio-dev.fr/_mcp \
--header "Authorization: Bearer <api-token>"
- En prod :
http://project.malio-dev.fr/_mcp - En réseau local :
http://<ip-serveur>:8082/_mcp
Où c'est stocké (si tu édites le fichier à la main, sous la clé mcpServers) :
| OS | Fichier de config Claude Code |
|---|---|
| Fedora / Linux | ~/.claude.json |
| Windows (collègue) | %USERPROFILE%\.claude.json (ex. C:\Users\<user>\.claude.json) |
| macOS | ~/.claude.json |
{
"mcpServers": {
"lesstime": {
"type": "http",
"url": "http://project.malio-dev.fr/_mcp",
"headers": { "Authorization": "Bearer <api-token>" }
}
}
}
Après modification, relancer la connexion avec /mcp dans Claude Code.
Gestion des tokens API
Générer / régénérer un token pour un utilisateur :
# En dev (container local)
docker exec -u www-data php-lesstime-fpm php bin/console app:generate-api-token <username>
# En prod (sur le serveur, dans infra/prod)
sudo docker compose exec -T -u www-data app php bin/console app:generate-api-token <username>
⚠️ Le token est invalidé à chaque reset/reseed de la base. Symptôme : /mcp renvoie
HTTP 401 "Invalid API token". Il faut alors le régénérer (commande ci-dessus) puis remplacer
la valeur Bearer ... dans ta config locale (par poste).
Déploiement
La prod tourne en Docker : l'image est buildée par la CI Gitea sur push de tag v*
(gitea.malio.fr/malio-dev/lesstime:<tag>), puis déployée par le script deploy.sh sur
le serveur (dossier /var/www/lesstime, container lesstime-app).
# Sur le serveur, depuis /var/www/lesstime
sudo ./deploy.sh # déploie la dernière image (latest)
sudo ./deploy.sh v0.4.2 # déploie une version précise
Le script active la maintenance, pull l'image, redémarre le container, lance les migrations
et vide le cache. Guide complet (première installation, BDD, Nginx, JWT, rollback) :
doc/deployment-docker.md.
Error tracking (GlitchTip)
Les erreurs backend et frontend sont remontées vers GlitchTip (instance auto-hébergée interne, compatible SDK Sentry) qui les groupe par projet et compte les occurrences. Activé uniquement en prod : en dev, sans DSN, le SDK est inerte (zéro impact). Ticket de référence : INFRA #146.
Pourquoi back et front se configurent différemment
| Backend (Symfony) | Frontend (Nuxt SPA) | |
|---|---|---|
| Nature | process PHP qui tourne en continu | fichiers JS/HTML statiques (nuxt generate) |
| Quand le DSN est lu | au runtime | figé au build (baké dans le JS) |
| Où mettre le DSN | .env du serveur (/var/www/lesstime/.env) — runtime |
secrets Gitea → build-args de la CI |
Les erreurs partent toujours vers GlitchTip, jamais vers la CI. La CI ne sert qu'à écrire le DSN front dans le bundle au moment du build (il n'y a aucun process front en prod qui pourrait lire une variable d'environnement).
Variables
Backend — fichier .env du serveur (/var/www/lesstime/.env, chargé via env_file ; le repo ne fournit que le template infra/prod/.env.example) :
SENTRY_DSN=http://<clé>@glitchtip.interne:<port>/<id-projet-api>
Frontend — secrets Gitea (repo → Settings → Actions → Secrets), consommés par
.gitea/workflows/build-docker.yml :
| Secret Gitea | Rôle |
|---|---|
SENTRY_FRONT_DSN |
DSN du projet lesstime-front (public, baké dans le JS) |
SENTRY_URL |
URL de l'instance GlitchTip |
SENTRY_ORG |
slug de l'organisation GlitchTip |
SENTRY_FRONT_PROJECT |
slug du projet front |
SENTRY_AUTH_TOKEN |
token d'upload des source maps (vrai secret) |
Sans source maps, seul
SENTRY_FRONT_DSNest requis (les stacktraces front seront sur du JS minifié). Le build n'échoue pas si les autres secrets sont absents.
Fichiers concernés
| Fichier | Rôle |
|---|---|
config/packages/sentry.yaml |
conf backend (prod-only, exceptions, 4xx ignorés, release = app.version) |
config/bundles.php |
SentryBundle enregistré ['prod' => true] |
frontend/nuxt.config.ts |
module Sentry chargé uniquement si DSN présent + upload source maps |
frontend/sentry.client.config.ts |
init du SDK client (no-op si DSN vide) |
infra/prod/Dockerfile |
build-args front (NUXT_PUBLIC_SENTRY_DSN, SENTRY_*) |
.gitea/workflows/build-docker.yml |
injection des secrets Gitea en build-args |
Activation (résumé)
- Dans GlitchTip : créer les projets
lesstime-apietlesstime-front, récupérer les 2 DSN (+ un auth token pour les source maps). - Backend : ajouter
SENTRY_DSNdans le.envdu serveur (/var/www/lesstime/.env). - Frontend : ajouter les secrets Gitea ci-dessus.
- Tagger une version (
v*) → la CI build l'image avec le DSN front baké →deploy.sh.
Certificat HTTPS interne (CA auto-signée)
GlitchTip est servi en HTTPS sur https://logs.malio-dev.fr (nginx devant), avec un certificat
auto-signé par une CA interne (« MALIO-DEV Local Root CA », cert serveur *.malio-dev.fr).
malio-dev.fr est un domaine interne uniquement (DNS local, pas de résolution publique).
Pourquoi pas Let's Encrypt ? Une CA publique doit valider le domaine via Internet (challenge HTTP ou DNS public). Comme
malio-dev.frn'existe qu'en interne, aucune validation n'est possible → on reste sur la CA interne, qu'il faut faire approuver partout où la connexion TLS est établie. Tant que la CA n'est pas approuvée, rien ne remonte : le backend logue « Message not sent » (SDK Sentry) et le navigateur affiche « connexion non sécurisée » (le front n'envoie rien).
Qui doit faire confiance à la CA ? La connexion à logs.malio-dev.fr part de deux endroits
différents, donc deux fixes distincts :
| Émetteur des erreurs | Qui établit le TLS | Où approuver la CA |
|---|---|---|
| Backend (Symfony) | le container PHP | CA bakée dans l'image Docker (ci-dessous) |
| Frontend (SPA) | le navigateur du poste | CA poussée sur les postes via GPO (ci-dessous) |
Fix backend — CA bakée dans l'image
Le certificat public de la root CA est committé dans le repo (infra/prod/malio-dev-root-ca.crt,
aucune clé privée) et installé dans le trust store du container au build (infra/prod/Dockerfile,
stage production — ca-certificates est déjà installé) :
COPY infra/prod/malio-dev-root-ca.crt /usr/local/share/ca-certificates/malio-dev-root-ca.crt
RUN update-ca-certificates
Le container fait alors confiance à tout *.malio-dev.fr interne et le SDK Sentry backend peut
envoyer. Vérification :
curl --cacert infra/prod/malio-dev-root-ca.crt https://logs.malio-dev.fr/api/1/store/ # → HTTP 200
Fix postes — CA poussée par GPO (Active Directory)
Le front est une SPA : c'est le navigateur de l'utilisateur qui contacte logs.malio-dev.fr,
donc c'est le poste qui doit faire confiance à la CA (la CA de l'image ne sert qu'au backend).
Sur le domaine Active Directory, on pousse la CA une seule fois via GPO plutôt que poste par poste :
- Contrôleur de domaine → Group Policy Management → éditer une GPO.
Configuration ordinateur → Stratégies → Paramètres Windows → Paramètres de sécurité → Stratégies de clé publique → Autorités de certification racines de confiance.- Clic droit → Importer → sélectionner
rootCA.crt(« MALIO-DEV Local Root CA »). - Sur les postes :
gpupdate /force(ou attendre le rafraîchissement), puis redémarrer le navigateur.
- Chrome / Edge utilisent le magasin Windows → confiance automatique.
- ⚠️ Firefox a son propre magasin : activer
security.enterprise_roots.enabled = true(about:configou via policy) pour qu'il lise le magasin Windows.
Validation poste : ouvrir
https://logs.malio-dev.fr→ cadenas vert sans avertissement = CA approuvée = le front peut envoyer.
Renouvellement / changement de CA
Si la CA interne change (rotation, expiration) :
- Remplacer
infra/prod/malio-dev-root-ca.crtpar le nouveau certificat public, commit + rebuild de l'image (re-tagv*) pour le backend. - Re-pousser la nouvelle CA via GPO (étapes ci-dessus) pour les postes.
Licence
Propriétaire — Tous droits réservés.