# 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

```bash
# 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_KEY` et `LOCK_DSN` sont introduites par l'intégration mail.
> Détails de config et cron de synchronisation : `docs/mail-integration.md` et `docs/mail-cron-setup.md`.
> Générer une clé : `php -r "echo bin2hex(random_bytes(32));"`.

## Commandes

### Docker

```bash
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

```bash
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

```bash
make migration-migrate  # Lancer les migrations
make fixtures           # Charger les fixtures
make db-reset           # Reset BDD + migrations + fixtures (⚠️ supprime les données)
```

### Tests & Qualité

```bash
make test                       # PHPUnit
make php-cs-fixer-allow-risky   # Fix code style PHP (Symfony + PSR-12)
```

### Installation complète

```bash
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_check` avec `{ username, password }` → cookie JWT `BEARER_LESSTIME`

## 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)

```json
{
  "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) :**

```bash
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` |

```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 :

```bash
# 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`).

```bash
# 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`) :
```env
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_DSN` est 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é)

1. Dans GlitchTip : créer les projets `lesstime-api` et `lesstime-front`, récupérer les 2 DSN
   (+ un auth token pour les source maps).
2. Backend : ajouter `SENTRY_DSN` dans le `.env` du serveur (`/var/www/lesstime/.env`).
3. Frontend : ajouter les secrets Gitea ci-dessus.
4. 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.fr` n'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é) :

```dockerfile
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 :

```bash
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 :

1. Contrôleur de domaine → **Group Policy Management** → éditer une GPO.
2. `Configuration ordinateur → Stratégies → Paramètres Windows → Paramètres de sécurité → Stratégies
   de clé publique → Autorités de certification racines de confiance`.
3. Clic droit → **Importer** → sélectionner `rootCA.crt` (« MALIO-DEV Local Root CA »).
4. 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:config` ou 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) :

1. Remplacer `infra/prod/malio-dev-root-ca.crt` par le nouveau certificat public, commit + **rebuild
   de l'image** (re-tag `v*`) pour le backend.
2. **Re-pousser** la nouvelle CA via GPO (étapes ci-dessus) pour les postes.

## Licence

Propriétaire — Tous droits réservés.
