Plateforme SaaS multi-tenant de gestion intelligente des plannings et des ressources humaines.
- Production : https://smartplanning.fr
- Version : 2.0
- Jira : prefixe
SP
| Couche | Technologies |
|---|---|
| Frontend | Next.js 15.5.25 (App Router), React 19, TypeScript 5.9, Tailwind + Shadcn/ui |
| Backend | NextAuth v5 (Auth.js), Prisma 6.18.0, Zod, Stripe v20.3.1 |
| Base de donnees | PostgreSQL 16, Redis 7 (ioredis 5.10) |
| Emails | React Email (30 envois transactionnels), Nodemailer SMTP, releve IMAP des rejets |
| Temps reel | Server-Sent Events (SSE) — notifications + messagerie sur un stream unique |
| DevOps | Docker, GitHub Actions (CI/CD), VPS OVH (Ubuntu 24.04), Nginx, Let's Encrypt |
- Authentification : Multi-roles (SYSTEM_ADMIN, DIRECTOR, MANAGER, EMPLOYEE), verification email, invitation par email, activation de compte
- Dashboards : 4 tableaux de bord par role avec KPIs, graphiques Recharts, animations Framer Motion
- Onboarding : Ecran de bienvenue a la premiere connexion (DIRECTOR) et checklist de demarrage contextuelle sur le dashboard (equipe -> employe -> planning -> profil), affichee tant que la configuration est incomplete
- Planning : Calendrier Schedule-X (drag & drop, recurrence, conflits, exports PDF/Excel/CSV), vues jour/semaine/mois. Les dates de creneau, de conge et d'indisponibilite sont stockees au jour calendaire de Paris, a 00:00 UTC, quelle que soit la forme envoyee par le navigateur : recurrences, jours ouvres et conflits ne dependent plus du fuseau du serveur (SP-609)
- Conges : Workflow validation (PENDING -> APPROVED/REJECTED), soldes CP/RTT, overlay calendrier, demi-journees
- Messagerie interne : Conversations DIRECT (1:1), TEAM (auto-sync equipes) et GROUP (manuelles). Messages texte + pieces jointes (PDF, images via Cloudinary, max 10 Mo). Reception temps reel via SSE. Groupement de messages, scroll infini cursor-based, optimistic updates. Archivage avec desarchivage auto sur nouveau message. Administration de groupe : avatar personnalisable, renommage, gestion des membres reserves a l'admin. SYSTEM_ADMIN peut contacter n'importe quel utilisateur cross-tenant (conversations avec
companyId: null, isolation multi-tenant preservee pour les autres roles). - Import CSV/Excel : Import bulk d'employes depuis fichier CSV ou Excel (.xlsx). Validation Zod temps reel cote client avec cellules colorees. Support headers FR/EN avec normalisation. Detection des doublons, creation auto des equipes, sync Stripe. Modele telecharger pre-rempli.
- Billing : Abonnement per-seat Stripe (2,90 euros/employe/mois), portail client, webhooks, sync employes auto. Essai gratuit 21 jours sans carte bancaire : aucun customer Stripe n'est cree avant la premiere souscription volontaire, le suivi de l'essai passe par une ligne
Subscriptionlocale (statut TRIAL) basculee en EXPIRED par le cron/api/cron/trial-emails, une fois la date de fin reellement depassee et non le jour meme.Subscription.quantitycompte les sieges factures chez Stripe : il vaut 0 tant qu'aucun abonnement n'existe, y compris pour une entreprise ayant des employes actifs - Notifications : Temps reel SSE, 30 emails transactionnels (React Email), preferences par categorie/canal. Destinataires bornes par
companyIda la selection ET refuses par une garde multi-tenant dans les factories : une notification ne peut pas partir vers une autre entreprise - Redis : Rate limiting distribue (INCR+EXPIRE), sessions actives (TTL 24h), cache dashboards (TTL 300s), fallback memoire
- CRUD : Entreprises, employes, equipes avec RBAC et multi-tenant strict. Corriger l'email d'un collaborateur propage le changement au compte de connexion : reinvitation immediate si le compte n'a jamais ete active, sinon confirmation par le collaborateur lui-meme (l'ancienne adresse reste l'identifiant jusqu'au clic, et recoit une alerte)
- Audit : Journal d'audit complet, export CSV, protection anti-injection
- Impersonation : Mode support SYSTEM_ADMIN "Voir espace client" lecture seule (cookie
sp-impersonationTTL 1h, audit trail start/stop, billing accessible en lecture, fallback cookie pour race condition JWT) - Monitoring : Health check DB + Redis (PING/PONG), KPIs SaaS, graphiques admin, service MRR unifie
- Delivrabilite : Un envoi accepte par le relais SMTP ne prouve pas la livraison.
EmailResult.outcomedistingue une adresse refusee (BOUNCED) d'une panne technique (FAILED), et le cron/api/cron/bounce-syncreleve la boite d'expedition toutes les 6 heures pour capter les refus asynchrones, qui arrivent plusieurs secondes apres l'acceptation. Le rapprochement avecEmailLogse fait par adresse et la releve couvre INBOX, la corbeille et les indesirables : les bounces y sont tries automatiquement et n'atteignent pas la boite de reception. Une adresse rejetee est signalee dans la liste des employes et dans l'espace admin - Admin : Page utilisateurs cross-tenant, essais a risque, broadcast email, stats + export PDF, journal des emails, suivi des messages de contact (
/app/admin/messages-contact, reserve au SYSTEM_ADMIN : la table ne porte pas decompanyId, l'expediteur etant un visiteur anonyme) - Profil : Avatar Cloudinary (affiche dans le header et la sidebar), RGPD (export donnees, suppression compte), preferences affichage. Le poste (
jobTitle) renseigne au profil s'affiche a la place du libelle de role dans l'interface - Settings : Apparence, notifications, entreprise (jours travailles, horaires)
- Notes & Incidents : Taches personnelles (drag & drop), notes d'incidents avec visibilite RBAC
- SEO / GEO : Metadata API, JSON-LD Schema.org (@graph, Article, HowTo, FAQPage, BreadcrumbList), sitemap data-driven avec
lastModifiedreels, robots.txt ouvert aux crawlers IA,llms.txtetllms-full.txt - Contenu editorial : Pages secteur
/solutions/[slug](restauration, commerce, BTP) et guides pratiques/guides/[slug], chacune sous son hub (/solutions,/guides), generes depuis des registres data-driven en SSG strict. Ajouter une page = 1 fichier de donnees + 1 ligne au registre (sitemap, footer, navigation et garde-fous de tests suivent automatiquement) - Pages publiques : Identite editoriale depuis SP-565 a SP-575, aplats pleine largeur, angles vifs, mode clair unique. Landing en dix sections numerotees, FAQ, page contact dediee (chaque demande est ecrite en base avant l'envoi des emails, une panne SMTP ne fait donc plus disparaitre de message), pages legales RGPD, demos video par role (Directeur/Manager/Employe) avec onglets et JSON-LD VideoObject. Le simulateur de tarifs vit sur
/tarifs, la landing y renvoie. Navigation en pattern disclosure (liens toujours dans le DOM pour le maillage interne,inerta l'etat ferme). Le fil d'Ariane est pose dans le hero et non dans une bande a lui : il garde sonBreadcrumbListsans couter de hauteur. Les pages d'authentification partagent la meme identite, leurs deux moities alignees en haut - Accessibilite : WCAG 2.1 AA, touch targets 44px, Lighthouse A11y 97-100%
Historique detaille du developpement :
docs/journal/
| Outil | Version minimale | Verification |
|---|---|---|
| Node.js | >= 20.0.0 | node -v |
| npm | >= 10.0.0 | npm -v |
| Docker | >= 24.0 | docker --version |
| Docker Compose | >= 2.0 | docker compose version |
git clone https://github.com/krismos64/SmartplanningAI-V2.git
cd SmartplanningAI-V2
npm install
cp .env.example .env.local # Configurer les variables
npm run docker:dev # PostgreSQL + Redis + Adminer (compose dans docker/)
npx prisma migrate dev
npx prisma generate
npm run db:seed # Donnees de demonstration
npm run devCopier .env.example vers .env.local et renseigner au minimum :
| Variable | Description | Obligatoire |
|---|---|---|
DATABASE_URL |
URL PostgreSQL (pre-rempli pour Docker local) | Oui |
AUTH_SECRET |
Cle JWT — openssl rand -base64 32 |
Oui |
AUTH_URL |
URL de l'app (http://localhost:3000) |
Oui |
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD |
Config SMTP pour emails transactionnels | Oui |
IMAP_HOST, IMAP_PORT, IMAP_USER, IMAP_PASSWORD |
Releve de la boite d'expedition pour detecter les adresses rejetees (SP-579). Memes identifiants que le SMTP chez Hostinger. Sans elles, /api/cron/bounce-sync repond 200 sans rien faire |
Non |
STRIPE_SECRET_KEY, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY |
Cles API Stripe (mode test en dev) | Oui |
STRIPE_WEBHOOK_SECRET |
Webhook Stripe — stripe listen --forward-to localhost:3000/api/webhooks/stripe |
Oui |
STRIPE_PRICE_ID |
Price ID du tarif per-seat | Oui |
CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET |
Upload avatars + pieces jointes messagerie | Oui |
REDIS_URL |
URL Redis (pre-rempli pour Docker local) | Oui |
CRON_SECRET |
Secret routes cron — openssl rand -base64 32 |
Oui |
HEALTH_API_KEY |
Secret endpoint /api/health |
Oui |
Voir .env.example pour la liste complete et les variables optionnelles (Umami, Sentry, OpenAI).
| Service | URL | Identifiants |
|---|---|---|
| Application | http://localhost:3000 | contact@smartplanning.fr / Password123! (admin) |
| Adminer | http://localhost:8080 | smartplanning / smartplanning / smartplanning |
| PostgreSQL | localhost:5432 | — |
| Redis | localhost:6379 | — |
Tous les comptes du seed partagent le mot de passe
Password123!. Voirprisma/seed.tspour la liste complete.
npm run db:seed ne fonctionne que sur une base vide : il part d'un
company.create() et echoue en P2002 sur une base deja peuplee. Il ne
complete pas une base existante.
Pour rejouer le seed apres qu'il a evolue (DESTRUCTIF, developpement uniquement) :
npx prisma migrate reset # vide, rejoue les migrations, puis le seedA faire des qu'un test E2E echoue sur un compte introuvable. Le seed est la
source de verite des comptes de test, et la CI le rejoue sur une base neuve a
chaque execution : une base locale qui derive fait rougir en local des specs
vertes en CI. C'est SP-595, ou unverified@techcorp.com a manque pendant plus
de trois mois apres son ajout.
npm run dev # Developpement
npm run build # Build production
npm run lint # ESLint
npm run type-check # TypeScript strict (tsc --noEmit)
npm run format # Prettier
npm run db:migrate # Migrations Prisma
npm run db:studio # Prisma Studio
npm run db:seed # Seed base de donnees
npm run test # Tests unitaires (single run)
npm run test:watch # Tests unitaires (watch)
npm run test:coverage # Couverture de tests
npm run test:e2e # Tests E2E Playwright (suite complete)
npm run test:e2e:ci # Tests E2E (whitelist CI)
npm run a11y:audit # Audit Lighthouse accessibilite
npm run email:dev # Previsualisation des templates React EmailPlaywright en local : prefixer par
PORT=3001si le port 3000 est deja occupe.
src/
├── app/ # Next.js 15 App Router (66 pages, 5 layouts, 18 API routes)
│ ├── (auth)/ # Login, register, verify-email, activate-account
│ ├── (about)/ # A propos, tarifs, contact
│ ├── (landing)/ # Landing page
│ ├── (legal)/ # Pages legales RGPD
│ ├── (sectors)/ # Pages secteur /solutions/[slug] (registre data-driven)
│ ├── (guides)/ # Hub et guides pratiques /guides/[slug] (registre data-driven)
│ ├── app/ # Routes protegees par role
│ └── api/ # API Routes (avatar, webhooks, health, SSE, messages...)
├── components/ # 187 composants React (.tsx hors tests)
│ ├── public/ # Primitives des pages publiques (identite editoriale)
│ ├── messaging/ # Messagerie (8 composants)
│ ├── import/ # Import CSV (2 composants + utilitaires)
│ └── ui/ # Shadcn/ui (41 composants)
├── lib/ # Actions (32), services (22), validations Zod, email (21 fichiers de templates)
├── hooks/ # 22 hooks custom (SSE, SWR, messagerie, import CSV, analytics)
├── types/ # Types TypeScript globaux
└── styles/ # Design tokens centralises
22 modeles Prisma (18 core + 4 NextAuth), 16 enums, 65 index, 24 migrations.
| Categorie | Modeles |
|---|---|
| Auth (NextAuth) | User, Account, Session, VerificationToken |
| Core | Company, Employee, Team |
| Planning | Schedule, Availability |
| Conges | LeaveRequest, LeaveBalance |
| Notes | PersonalTask, IncidentNote |
| Messagerie | Conversation, ConversationMember, Message |
| Systeme | Notification, Subscription, Payment, AuditLog, EmailLog, ContactMessage |
Voir docs/database-architecture.md pour le detail complet.
| Type | Framework | Fichiers | Tests |
|---|---|---|---|
| Unitaires | Vitest | 210 | 3 443 |
| E2E | Playwright | 25 | 271 |
| Total | 235 | 3 714 |
Compteurs mesures le 2 octobre 2026, par npm run test et
npx playwright test --list. Ils se periment a chaque sprint : les remesurer
plutot que les recopier.
La couverture est un garde-fou bloquant depuis SP-592, et pas seulement une
commande d'information. vitest.config.ts porte des seuils mesures (lines 50,
branches 73, functions 73, statements 50, pour un reel a 53,27 % le 2 octobre 2026) et la CI lance
npm run test:coverage : elle rougit si la couverture baisse, meme quand tous
les tests passent. Le perimetre mesure inclut src/lib/, src/hooks/ et
src/lib/validations/, et exclut les pages et layouts, couverts par les E2E.
Relever un seuil demande de mesurer d'abord, jamais de viser un chiffre rond.
La CI execute une whitelist E2E (11 specs, 139 tests) ; la suite complete (25 specs, 271 tests) tourne en nightly. testMatch de playwright.ci.config.ts etant une liste explicite, un spec renomme ou supprime disparait silencieusement de la CI : verifier cette liste apres chaque ajout ou suppression.
Les specs publiques ne sont pas dans la whitelist CI. Les 26 tests de
e2e/specs/landing/, dont 11 audits axe-core, ne tournent donc qu'en nightly
et en local (npx playwright test e2e/specs/landing/). L'arbitrage sur leur
entree en CI n'est pas tranche.
Focus sur la logique metier critique : RBAC, Zod, Server Actions, Stripe, workflows E2E, messagerie, import CSV, registres SEO/GEO.
| Element | Valeur |
|---|---|
| URL | https://smartplanning.fr |
| Serveur | VPS OVH (4 vCores, 8GB RAM) |
| OS | Ubuntu 24.04 LTS |
| Containers | Docker Compose (app + PostgreSQL 16 + Redis 7) |
| Analytics | Umami (analytics.smartplanning.fr) |
| Registry | GitHub Container Registry (ghcr.io) |
Push main → CI (lint + tests + build) → CD (Docker build → Prisma migrate → deploy VPS)
L'ordre migrate puis deploy est deliberé (SP-523) : l'ancien ordre laissait
une fenetre ou le nouveau code tournait avec l'ancien schema.
| Trigger | Tests | Deploiement | Temps |
|---|---|---|---|
| Push feature (sans PR) | Aucun | Non | — |
| PR vers main | Unitaires + E2E (whitelist CI) | Non | ~15-18 min |
| Push direct / merge main | Unitaires + E2E (whitelist CI) | Oui (auto) | ~15-18 min |
| Nightly (2h UTC) | Unitaires + E2E complet | Non | ~45-60 min |
Depuis la revision de juillet 2026, un push sur une branche sans PR ne declenche plus le CI : auparavant, chaque push sur une branche avec PR ouverte lancait deux runs complets pour le meme commit (evenements push et pull_request). Ouvrir une PR, meme en draft, donne le feedback CI.
Le CD ne se declenche que si le CI reussit entierement, E2E comprises : meme un push direct sur main passe par les tests avant deploiement. Les migrations Prisma sont executees dans un conteneur ephemere, avant le remplacement du conteneur applicatif.
Un deploiement en echec est rouge, et la production restauree (SP-588). Si le healthcheck ne repond pas dans les 150 secondes, le script remet l'image precedente, verifie qu'elle repond, puis sort en erreur. Auparavant il ecrivait « Deploiement termine (avec warnings) » et le job reussissait : un conteneur qui ne demarrait pas devenait la production, annoncee comme un succes.
Le deploiement vise sha-<commit court>, expose en sortie du job de build
plutot que reconstruit, et une clause concurrency empeche deux deploiements
simultanes. Le rollback restaure le code, jamais le schema : une migration
destructive se decoupe en expand puis contract.
Les logs de l'application survivent aux deploiements (SP-607). Le service
app ecrit dans journald et non dans le json-file de Docker, efface avec le
conteneur que le CD recree a chaque mise en production. Lecture, conteneurs
precedents compris : sudo journalctl CONTAINER_NAME=smartplanning-app, et
-p err pour les seules erreurs.
Guide complet :
docs/deployment.md
Le DNS du domaine est gere chez Hostinger alors que le serveur est chez OVH. Une bascule de la zone DNS rend le site inaccessible sans que rien ne casse cote VPS : c'est la panne du 18 aout 2026, ou le trafic partait vers un CDN tiers servant un certificat expire pendant que certbot renouvelait correctement.
scripts/ops/check-tls-expiry.sh tourne en cron sur le VPS deux fois par jour
et alerte par email. Il interroge le certificat tel qu'il est servi en
HTTPS, resolution DNS comprise, un controle lisant /etc/letsencrypt/
n'ayant rien vu de cette panne. Voir scripts/ops/README.md.
Le VPS heberge un second projet depuis septembre 2026. Les conteneurs publient
donc leurs ports sur la boucle locale (127.0.0.1:3000:3000), Nginx restant le
seul point d'entree.
UFW ne filtre pas les ports publies par Docker, qui ecrit ses regles en amont de
sa chaine : un port peut repondre depuis Internet alors qu'ufw status le dit
ferme. C'est SP-583, ou l'application et Umami etaient joignables sur les ports
3000 et 3001 de l'adresse publique, hors TLS et hors limitation de debit.
Un port se verifie depuis une autre machine, jamais depuis le VPS ou
curl localhost:3000 repond toujours 200 sans rien prouver.
Rien n'impose cette convention mecaniquement : la chaine DOCKER-USER est vide,
donc une ligne mal ecrite dans un futur compose rouvrirait le port.
scripts/ops/check-public-ports.sh tourne en cron une fois par jour et le
detecte, en interrogeant l'adresse publique du VPS et non localhost.
Jusqu'au 9 septembre 2026, la base de production n'etait sauvegardee nulle part : aucune tache cron, aucun timer, aucun fichier de dump. Le constat a ete fait en verifiant une affirmation de la politique de confidentialite (SP-593).
scripts/ops/backup-database.sh tourne desormais chaque nuit a 03:20 UTC par
smartplanning-backup.timer. Il produit un dump au format custom, verifie son
integrite par pg_restore --list, le chiffre en GPG AES256, controle que le
fichier chiffre se dechiffre bien, puis fait la rotation sur 30 jours. Chaque
etape qui ne peut pas conclure arrete le script en erreur.
Le chiffrement n'est pas cosmetique : le disque du VPS n'est pas chiffre
(ext4 nu) et la machine est partagee avec un second projet. Un dump en clair y
serait une base de donnees personnelles lisible par tout acces fichier.
Une sauvegarde jamais restauree ne prouve rien.
scripts/ops/test-backup-restore.sh restaure la derniere archive dans une base
temporaire, compte les objets, puis la supprime. A lancer periodiquement, et
avant toute migration risquee.
Procedure de restauration :
docs/runbooks/restauration-base-production.md.
Copie hors site depuis SP-594. scripts/ops/sync-backups-offsite.sh envoie
chaque nuit a 04:10 UTC la derniere archive vers Backblaze B2, fournisseur
volontairement distinct d'OVH : un stockage objet OVH aurait couvert le disque
mort, pas la panne du fournisseur. L'envoi est verifie taille et empreinte
SHA-1 relues depuis B2, et le script refuse une archive de plus de 48 h, sinon
un hors-site paraitrait sain alors que la sauvegarde locale a cesse.
La cle de chiffrement vit dans le gestionnaire de mots de passe et sur le poste de developpement, jamais uniquement sur le VPS : une cle rangee a cote de ce qu'elle protege ne protege rien.
Restauration depuis le hors-site prouvee le 10 septembre 2026 sur une machine autre que le VPS : 23 tables, 200 objets, dix comptages identiques a la production.
La copie hors site masque, elle ne supprime plus (SP-597). La cle B2 qui vit
sur le VPS portait deleteFiles : un rancongiciel qui prend la machine chiffrait
la base, puis se servait de cette cle pour effacer les copies distantes, rendant
le dispositif inutile au moment precis ou il servirait.
La rotation appelle desormais b2_hide_file, qui pose un marqueur sans detruire
un octet, et l'effacement reel est confie a une regle de cycle de vie du
compartiment, masquage a 30 jours puis effacement 1 jour apres, qui s'applique
meme serveur eteint. La cle est reduite a quatre capacites, listBuckets,
listFiles, readFiles et writeFiles.
Deux scripts encadrent l'operation : rotate-b2-key.sh cree la cle restreinte
et refuse de basculer si la regle de cycle de vie manque,
check-b2-key-hardening.sh prouve le durcissement par un test negatif sur un
fichier reel. Il ne se lance qu'apres la bascule : l'API B2 n'offre aucun
dry run pour b2_delete_file_version, donc lance sur une cle non durcie, il
detruit vraiment.
Consequence pour une restauration : une archive masquee disparait de la liste par
nom mais reste entierement telechargeable par son fileId, marche a suivre dans
le runbook.
Limite qui subsiste : le disque du VPS n'est pas chiffre (ext4 nu, aucun
volume LUKS), donc un acces fichier sur la machine donne acces a la cle locale.
Le chiffrement au repos reste une decision d'architecture, sans ticket.
- RBAC 4 niveaux avec
checkPermission()sur chaque Server Action - Isolation multi-tenant par
companyIdsur chaque requete Prisma (defense-in-depth) - Validation Zod aux frontieres, protection CSRF (NextAuth)
- Cookies httpOnly + secure + sameSite, hashage bcrypt
- Rate limiting Redis distribue (fallback memoire), audit logs, CSP headers, SRI en production
- Subscription guard middleware Edge Runtime
- Verification email a l'inscription (token 24h, page
/verify-email). Envoi limite a trois par heure et par adresse, inscription comprise, avec une reponse identique au-dela de la limite pour ne pas reveler l'existence du compte (SP-606) - Emails securite envoyes inconditionnellement (changement mot de passe, suppression RGPD)
- Messagerie : messages prives par conversation, isolation multi-tenant, verification membership sur chaque action
- Sauvegardes quotidiennes chiffrees (AES256), restauration verifiee (SP-593), copiees hors du VPS chaque nuit vers Backblaze B2 avec restauration prouvee sur une autre machine (SP-594). La cle qui vit sur le VPS ne peut plus detruire l'historique distant, refus prouve en 401 sur un fichier reel (SP-597)
- Domaines d'images distantes restreints a Cloudinary :
hostname: '**'ouvrait l'optimiseurnext/imagea n'importe quel domaine HTTPS (SP-589) - Import de fichiers plafonne a 5 Mo, verifie avant lecture (SP-590)
- Le middleware exige une identite exploitable (
auth.user.id) et non la seule presence d'un objet de session (SP-589)
Documentation securite :
docs/security/
| Page | Performance | SEO | Accessibilite | Best Practices |
|---|---|---|---|---|
| Landing (mesure mai 2026) | 91% | 100% | 100% | 96% |
/solutions/planning-restaurant |
— | 100% | 97% | 100% |
Mesures ponctuelles, non rejouees a chaque build : les rejouer apres toute modification d'une page publique.
docs/deployment.md— Guide de deploiement VPSdocs/database-architecture.md— Architecture BDD (22 modeles, 16 enums)docs/journal/— Journal de developpement, une entree par sessiondocs/analytics.md: Configuration Umami et tunnel de conversiondocs/runbooks/: Procedures d'exploitation, dont la restauration de la basedocs/security/— Plan de securisation, incidents, hardeningscripts/ops/README.md: Scripts de production, sauvegarde, surveillance TLS et ports.claude/— Configuration de l'assistant : regles, hooks et conventions du projet
Christophe Mostefaoui — Developpeur full-stack freelance
Projet realise dans le cadre du titre professionnel Concepteur Developpeur d'Applications (CDA).
Ce projet est proprietaire. Tous droits reserves.