diff --git a/.env.example b/.env.example index 9627e79..4e822b3 100644 --- a/.env.example +++ b/.env.example @@ -91,6 +91,20 @@ ENCRYPTION_KEY=change_me_in_env_local # POSTGRES_PORT=5435 # XDEBUG_CLIENT_HOST=host.docker.internal +# =========================================================================== +# Error tracking — GlitchTip (compatible SDK Sentry) +# =========================================================================== + +# DSN du projet GlitchTip "lesstime-api" (BACKEND, runtime). +# Actif uniquement en prod (bundle prod-only). Vide/absent => Sentry inerte. +# A definir dans infra/prod/.env (pas en dev). Ex : http://@glitchtip.interne:/ +# SENTRY_DSN= + +# NB : le DSN FRONT (lesstime-front) et l'upload des source maps sont fournis +# au BUILD de l'image, pas au runtime. Voir infra/prod/Dockerfile (ARG) et la +# CI .gitea/workflows/build-docker.yml (build-args depuis les secrets Gitea) : +# NUXT_PUBLIC_SENTRY_DSN, SENTRY_URL, SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN + # =========================================================================== # Frontend (frontend/.env) # =========================================================================== diff --git a/.gitea/workflows/build-docker.yml b/.gitea/workflows/build-docker.yml index 2b0b3f8..d5913ad 100644 --- a/.gitea/workflows/build-docker.yml +++ b/.gitea/workflows/build-docker.yml @@ -20,6 +20,11 @@ jobs: run: | docker build \ -f infra/prod/Dockerfile \ + --build-arg NUXT_PUBLIC_SENTRY_DSN="${{ secrets.SENTRY_FRONT_DSN }}" \ + --build-arg SENTRY_URL="${{ secrets.SENTRY_URL }}" \ + --build-arg SENTRY_ORG="${{ secrets.SENTRY_ORG }}" \ + --build-arg SENTRY_PROJECT="${{ secrets.SENTRY_FRONT_PROJECT }}" \ + --build-arg SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}" \ -t gitea.malio.fr/malio-dev/lesstime:${{ gitea.ref_name }} \ -t gitea.malio.fr/malio-dev/lesstime:latest \ . diff --git a/.mcp.json b/.mcp.json index aeea346..dbf183b 100644 --- a/.mcp.json +++ b/.mcp.json @@ -1,12 +1,5 @@ { "mcpServers": { - "lesstime": { - "type": "http", - "url": "http://project.malio-dev.fr/_mcp", - "headers": { - "Authorization": "Bearer 7e8b410a5b79b5c0432951dcee3a3a81e0731e86d9f70d8784ec079a2b759c64" - } - }, "lesstime-local": { "command": "docker", "args": [ diff --git a/README.md b/README.md index ba7b601..926ed2a 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Application de gestion de projet avec suivi du temps et portail client. - 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 @@ -74,6 +75,7 @@ peuvent être surchargées dans `.env.local` (jamais committé). En prod, elles | `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`. @@ -217,28 +219,60 @@ Lesstime expose un serveur MCP (Model Context Protocol) permettant aux assistant } ``` -### Configuration réseau (HTTP) +### 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 " +``` +- En prod : `http://project.malio-dev.fr/_mcp` +- En réseau local : `http://: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\\.claude.json`) | +| **macOS** | `~/.claude.json` | ```json { "mcpServers": { "lesstime": { - "type": "url", - "url": "http://:8082/_mcp", - "headers": { - "Authorization": "Bearer " - } + "type": "http", + "url": "http://project.malio-dev.fr/_mcp", + "headers": { "Authorization": "Bearer " } } } } ``` +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 + +# En prod (sur le serveur, dans infra/prod) +sudo docker compose exec -T -u www-data app php bin/console app:generate-api-token ``` +⚠️ 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*` @@ -255,6 +289,131 @@ Le script active la maintenance, pull l'image, redémarre le container, lance le 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://@glitchtip.interne:/ +``` + +**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. diff --git a/composer.json b/composer.json index 7b9c607..c298ae7 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "phpoffice/phpspreadsheet": "^5.5", "phpstan/phpdoc-parser": "^2.3", "sabre/vobject": "^4.5", + "sentry/sentry-symfony": "^5.10", "symfony/asset": "8.0.*", "symfony/console": "8.0.*", "symfony/doctrine-messenger": "^8.0", diff --git a/composer.lock b/composer.lock index 844faa7..1beb069 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "eee87b9c0011fb88523cb5aea0de29ba", + "content-hash": "106755bef51fd069316cd7f3a7e1a0b6", "packages": [ { "name": "api-platform/doctrine-common", @@ -2508,6 +2508,125 @@ }, "time": "2026-02-08T16:21:46+00:00" }, + { + "name": "guzzlehttp/psr7", + "version": "2.12.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7ec62dc3f44aa218487dbed81a9bf9bc647be55d", + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "1.1.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.12.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2026-06-23T15:21:08+00:00" + }, { "name": "icewind/smb", "version": "3.8.1", @@ -2960,6 +3079,66 @@ }, "time": "2026-05-04T12:34:54+00:00" }, + { + "name": "jean85/pretty-package-versions", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", + "vimeo/psalm": "^4.3 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" + }, + "time": "2025-03-19T14:43:43+00:00" + }, { "name": "lcobucci/jwt", "version": "5.6.0", @@ -4939,6 +5118,50 @@ }, "time": "2021-10-29T13:26:27+00:00" }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, { "name": "sabre/uri", "version": "3.0.2", @@ -5172,6 +5395,201 @@ }, "time": "2024-09-06T08:00:55+00:00" }, + { + "name": "sentry/sentry", + "version": "4.28.0", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-php.git", + "reference": "662cb7a01a342a7f33780fc955ff4a028d8b785a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/662cb7a01a342a7f33780fc955ff4a028d8b785a", + "reference": "662cb7a01a342a7f33780fc955ff4a028d8b785a", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "guzzlehttp/psr7": "^1.8.4|^2.1.1", + "jean85/pretty-package-versions": "^1.5|^2.0.4", + "php": "^7.2|^8.0", + "psr/log": "^1.0|^2.0|^3.0", + "symfony/options-resolver": "^4.4.30|^5.0.11|^6.0|^7.0|^8.0" + }, + "conflict": { + "raven/raven": "*" + }, + "require-dev": { + "carthage-software/mago": "1.30.0", + "friendsofphp/php-cs-fixer": "^3.4", + "guzzlehttp/promises": "^2.0.3", + "monolog/monolog": "^1.6|^2.0|^3.0", + "nyholm/psr7": "^1.8", + "open-telemetry/api": "^1.0", + "open-telemetry/exporter-otlp": "^1.0", + "open-telemetry/sdk": "^1.0", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^8.5.52|^9.6.34", + "spiral/roadrunner-http": "^3.6", + "spiral/roadrunner-worker": "^3.6" + }, + "suggest": { + "ext-excimer": "Enable Sentry profiling with the Excimer PHP extension.", + "monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler." + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Sentry\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sentry", + "email": "accounts@sentry.io" + } + ], + "description": "PHP SDK for Sentry (http://sentry.io)", + "homepage": "http://sentry.io", + "keywords": [ + "crash-reporting", + "crash-reports", + "error-handler", + "error-monitoring", + "log", + "logging", + "profiling", + "sentry", + "tracing" + ], + "support": { + "issues": "https://github.com/getsentry/sentry-php/issues", + "source": "https://github.com/getsentry/sentry-php/tree/4.28.0" + }, + "funding": [ + { + "url": "https://sentry.io/", + "type": "custom" + }, + { + "url": "https://sentry.io/pricing/", + "type": "custom" + } + ], + "time": "2026-06-11T12:22:38+00:00" + }, + { + "name": "sentry/sentry-symfony", + "version": "5.10.0", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-symfony.git", + "reference": "6f49255f4cdcfc43a3a283bd3a1f65d483e9192f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-symfony/zipball/6f49255f4cdcfc43a3a283bd3a1f65d483e9192f", + "reference": "6f49255f4cdcfc43a3a283bd3a1f65d483e9192f", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^2.1.1", + "jean85/pretty-package-versions": "^1.5||^2.0", + "php": "^7.2||^8.0", + "sentry/sentry": "^4.23.0", + "symfony/cache-contracts": "^1.1||^2.4||^3.0", + "symfony/config": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/console": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/dependency-injection": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/event-dispatcher": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/http-kernel": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/polyfill-php80": "^1.22", + "symfony/psr-http-message-bridge": "^1.2||^2.0||^6.4||^7.0||^8.0", + "symfony/yaml": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0" + }, + "require-dev": { + "doctrine/dbal": "^2.13||^3.3||^4.0", + "doctrine/doctrine-bundle": "^2.6||^3.0", + "friendsofphp/php-cs-fixer": "^2.19||^3.40", + "masterminds/html5": "^2.8", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "1.12.5", + "phpstan/phpstan-phpunit": "1.4.0", + "phpstan/phpstan-symfony": "1.4.10", + "phpunit/phpunit": "^8.5.40||^9.6.21", + "symfony/browser-kit": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/cache": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/dom-crawler": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/framework-bundle": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/http-client": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/messenger": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/monolog-bundle": "^3.4||^4.0", + "symfony/phpunit-bridge": "^5.2.6||^6.0||^7.0||^8.0", + "symfony/process": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/security-core": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/security-http": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/twig-bundle": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "vimeo/psalm": "^4.3||^5.16.0" + }, + "suggest": { + "doctrine/doctrine-bundle": "Allow distributed tracing of database queries using Sentry.", + "monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler.", + "symfony/cache": "Allow distributed tracing of cache pools using Sentry.", + "symfony/twig-bundle": "Allow distributed tracing of Twig template rendering using Sentry." + }, + "type": "symfony-bundle", + "autoload": { + "files": [ + "src/aliases.php" + ], + "psr-4": { + "Sentry\\SentryBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sentry", + "email": "accounts@sentry.io" + } + ], + "description": "Symfony integration for Sentry (http://getsentry.com)", + "homepage": "http://getsentry.com", + "keywords": [ + "errors", + "logging", + "sentry", + "symfony" + ], + "support": { + "issues": "https://github.com/getsentry/sentry-symfony/issues", + "source": "https://github.com/getsentry/sentry-symfony/tree/5.10.0" + }, + "funding": [ + { + "url": "https://sentry.io/", + "type": "custom" + }, + { + "url": "https://sentry.io/pricing/", + "type": "custom" + } + ], + "time": "2026-04-01T14:50:32+00:00" + }, { "name": "symfony/asset", "version": "v8.0.6", diff --git a/config/bundles.php b/config/bundles.php index 3bbe71a..a5c0576 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -8,6 +8,7 @@ use Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle; use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle; use Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle; use Nelmio\CorsBundle\NelmioCorsBundle; +use Sentry\SentryBundle\SentryBundle; use Symfony\AI\McpBundle\McpBundle; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\MonologBundle\MonologBundle; @@ -24,4 +25,5 @@ return [ LexikJWTAuthenticationBundle::class => ['all' => true], McpBundle::class => ['all' => true], MonologBundle::class => ['all' => true], + SentryBundle::class => ['prod' => true], ]; diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 24cb6de..fdfe255 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -22,6 +22,7 @@ security: pattern: ^/login_check stateless: true provider: app_user_provider + user_checker: App\Module\Core\Infrastructure\Security\ArchivedUserChecker login_throttling: max_attempts: 5 interval: '1 minute' @@ -41,6 +42,7 @@ security: pattern: ^/api stateless: true provider: app_user_provider + user_checker: App\Module\Core\Infrastructure\Security\ArchivedUserChecker jwt: ~ logout: path: /api/logout diff --git a/config/packages/sentry.yaml b/config/packages/sentry.yaml new file mode 100644 index 0000000..cbc4938 --- /dev/null +++ b/config/packages/sentry.yaml @@ -0,0 +1,23 @@ +# Error tracking → GlitchTip (compatible SDK Sentry). +# Actif uniquement en prod (bundle enregistre seulement pour prod dans bundles.php). +# Si SENTRY_DSN est vide/non defini, le SDK est inerte (rien n'est envoye). +when@prod: + parameters: + # Valeur par defaut : DSN vide => Sentry desactive tant qu'il n'est pas fourni. + env(SENTRY_DSN): '' + + sentry: + dsn: '%env(SENTRY_DSN)%' + # Capture les exceptions levees par le kernel (comportement par defaut). + register_error_listener: true + register_error_handler: true + options: + environment: '%env(APP_ENV)%' + release: '%app.version%' + # Pas d'APM/tracing (DuckDB hors perimetre du ticket #146). + traces_sample_rate: 0.0 + # Ne pas remonter les 4xx HTTP comme des erreurs (bruit). + ignore_exceptions: + - Symfony\Component\HttpKernel\Exception\NotFoundHttpException + - Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException + - Symfony\Component\Security\Core\Exception\AccessDeniedException diff --git a/config/reference.php b/config/reference.php index 145877b..8733dbe 100644 --- a/config/reference.php +++ b/config/reference.php @@ -1752,6 +1752,90 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * }, * }>, * } + * @psalm-type SentryConfig = array{ + * dsn?: scalar|Param|null, // If this value is not provided, the SDK will try to read it from the SENTRY_DSN environment variable. If that variable also does not exist, the SDK will not send any events. + * register_error_listener?: bool|Param, // Default: true + * register_error_handler?: bool|Param, // Default: true + * logger?: scalar|Param|null, // The service ID of the PSR-3 logger used to log messages coming from the SDK client. Be aware that setting the same logger of the application may create a circular loop when an event fails to be sent. // Default: null + * options?: array{ + * integrations?: mixed, // Default: [] + * default_integrations?: bool|Param, + * prefixes?: list, + * sample_rate?: float|Param, // The sampling factor to apply to events. A value of 0 will deny sending any event, and a value of 1 will send all events. + * enable_tracing?: bool|Param, + * traces_sample_rate?: float|Param, // The sampling factor to apply to transactions. A value of 0 will deny sending any transaction, and a value of 1 will send all transactions. + * traces_sampler?: scalar|Param|null, + * profiles_sample_rate?: float|Param, // The sampling factor to apply to profiles. A value of 0 will deny sending any profiles, and a value of 1 will send all profiles. Profiles are sampled in relation to traces_sample_rate + * enable_logs?: bool|Param, + * log_flush_threshold?: mixed, // Default: null + * enable_metrics?: bool|Param, // Default: true + * attach_stacktrace?: bool|Param, + * attach_metric_code_locations?: bool|Param, + * context_lines?: int|Param, + * environment?: scalar|Param|null, // Default: "%kernel.environment%" + * logger?: scalar|Param|null, + * spotlight?: bool|Param, + * spotlight_url?: scalar|Param|null, + * release?: scalar|Param|null, // Default: "%env(default::SENTRY_RELEASE)%" + * org_id?: int|Param, + * server_name?: scalar|Param|null, + * ignore_exceptions?: list, + * ignore_transactions?: list, + * before_send?: scalar|Param|null, + * before_send_transaction?: scalar|Param|null, + * before_send_check_in?: scalar|Param|null, + * before_send_metrics?: scalar|Param|null, + * before_send_log?: scalar|Param|null, + * before_send_metric?: scalar|Param|null, + * trace_propagation_targets?: mixed, + * strict_trace_continuation?: bool|Param, + * tags?: array, + * error_types?: scalar|Param|null, + * max_breadcrumbs?: int|Param, + * before_breadcrumb?: mixed, + * in_app_exclude?: list, + * in_app_include?: list, + * send_default_pii?: bool|Param, + * max_value_length?: int|Param, + * transport?: scalar|Param|null, + * http_client?: scalar|Param|null, + * http_proxy?: scalar|Param|null, + * http_proxy_authentication?: scalar|Param|null, + * http_connect_timeout?: float|Param, // The maximum number of seconds to wait while trying to connect to a server. It works only when using the default transport. + * http_timeout?: float|Param, // The maximum execution time for the request+response as a whole. It works only when using the default transport. + * http_ssl_verify_peer?: bool|Param, + * http_compression?: bool|Param, + * capture_silenced_errors?: bool|Param, + * max_request_body_size?: "none"|"never"|"small"|"medium"|"always"|Param, + * class_serializers?: array, + * }, + * messenger?: bool|array{ + * enabled?: bool|Param, // Default: true + * capture_soft_fails?: bool|Param, // Default: true + * isolate_breadcrumbs_by_message?: bool|Param, // Default: false + * isolate_context_by_message?: bool|Param, // Default: false + * }, + * tracing?: bool|array{ + * enabled?: bool|Param, // Default: true + * dbal?: bool|array{ + * enabled?: bool|Param, // Default: true + * ignore_prepare_spans?: bool|Param, // Default: false + * connections?: list, + * }, + * twig?: bool|array{ + * enabled?: bool|Param, // Default: false + * }, + * cache?: bool|array{ + * enabled?: bool|Param, // Default: true + * }, + * http_client?: bool|array{ + * enabled?: bool|Param, // Default: true + * }, + * console?: array{ + * excluded_commands?: list, + * }, + * }, + * } * @psalm-type ConfigType = array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, @@ -1792,6 +1876,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * lexik_jwt_authentication?: LexikJwtAuthenticationConfig, * mcp?: McpConfig, * monolog?: MonologConfig, + * sentry?: SentryConfig, * }, * "when@test"?: array{ * imports?: ImportsConfig, diff --git a/config/sidebar.php b/config/sidebar.php index b4761d4..75e1312 100644 --- a/config/sidebar.php +++ b/config/sidebar.php @@ -26,7 +26,14 @@ return [ ['label' => 'sidebar.general.myTasks', 'to' => '/my-tasks', 'icon' => 'mdi:clipboard-check-outline', 'module' => 'project-management', 'permission' => 'project-management.tasks.view'], ['label' => 'sidebar.general.projects', 'to' => '/projects', 'icon' => 'mdi:folder-outline', 'module' => 'project-management', 'permission' => 'project-management.projects.view'], ['label' => 'sidebar.general.timeTracking', 'to' => '/time-tracking', 'icon' => 'mdi:calendar-edit-outline', 'module' => 'time-tracking', 'permission' => 'time-tracking.entries.view'], - // Gating module uniquement (cf. en-tête) : rendu visuel + badge gérés côté layout. + ], + ], + [ + 'label' => 'sidebar.tools.section', + 'icon' => 'mdi:tools', + 'items' => [ + // Gating module uniquement : rendu visuel + badge non-lus gérés côté layout + // (filtré de translatedSections puis ré-injecté avec suffixe (N)). ['label' => 'sidebar.general.mail', 'to' => '/mail', 'icon' => 'mdi:email-outline', 'module' => 'mail'], ], ], @@ -37,8 +44,8 @@ return [ 'items' => [ ['label' => 'sidebar.admin.teamAbsences', 'to' => '/team-absences', 'icon' => 'mdi:calendar-account-outline', 'module' => 'absence'], ['label' => 'sidebar.admin.directory', 'to' => '/directory', 'icon' => 'mdi:card-account-details-outline', 'module' => 'directory'], - ['label' => 'sidebar.admin.administration', 'to' => '/admin', 'icon' => 'mdi:cog-outline', 'permission' => 'core.users.view'], ['label' => 'sidebar.admin.reporting', 'to' => '/reporting', 'icon' => 'mdi:chart-line', 'module' => 'reporting', 'permission' => 'reporting.view'], + ['label' => 'sidebar.admin.administration', 'to' => '/admin', 'icon' => 'mdi:cog-outline', 'permission' => 'core.users.view'], ], ], ]; diff --git a/config/version.yaml b/config/version.yaml index 350bb88..1644873 100644 --- a/config/version.yaml +++ b/config/version.yaml @@ -1,2 +1,2 @@ parameters: - app.version: '0.4.38' + app.version: '0.4.45' diff --git a/docs/superpowers/plans/2026-06-25-malio-sidebar-migration.md b/docs/superpowers/plans/2026-06-25-malio-sidebar-migration.md new file mode 100644 index 0000000..fcb0ada --- /dev/null +++ b/docs/superpowers/plans/2026-06-25-malio-sidebar-migration.md @@ -0,0 +1,484 @@ +# Migration sidebar vers MalioSidebar — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Remplacer la sidebar maison de Lesstime par le composant `MalioSidebar` de `@malio/layer-ui`, en 3 groupes (Général / Outils / Administration), avec timer + version dans le footer et le logo Malio de Starseed. + +**Architecture:** Modèle backend-driven conservé — `config/sidebar.php` filtré par `SidebarProvider` (permissions/rôles/modules côté serveur), exposé via `/api/sidebar`, consommé par `useSidebar()`. Le layout `default.vue` mappe ces sections vers le format `MalioSidebar` et fusionne les items contextuels rendus côté client (Kanban/Groupes/Archives, Documents, Mail+badge, Mes absences). + +**Tech Stack:** Nuxt 4 (SPA), Vue 3 ` - - diff --git a/frontend/components/ui/AppTopNav.vue b/frontend/components/ui/AppTopNav.vue index b8fdef2..05b77fd 100644 --- a/frontend/components/ui/AppTopNav.vue +++ b/frontend/components/ui/AppTopNav.vue @@ -3,11 +3,11 @@
diff --git a/frontend/modules/mail/pages/mail.vue b/frontend/modules/mail/pages/mail.vue index 2690254..c429359 100644 --- a/frontend/modules/mail/pages/mail.vue +++ b/frontend/modules/mail/pages/mail.vue @@ -95,11 +95,13 @@ function handleTaskLinked(_taskId: number): void {