Arnaud f1df7ae1b5 feat(admin): bouton Éditer sur la fiche projet (mode admin)
La fiche projet publique /projects/:slug avait déjà des actions inline
en mode admin (Supprimer, ajouter étape, ajouter ressource, etc.), mais
pas de raccourci pour basculer vers le form d'édition complet où on
peut changer titre, slug, catégorie, status, descriptions, progression.

Ajout d'un bouton "Éditer" à côté de "Supprimer" en haut à droite (admin
only) qui pointe vers /admin/projects/:slug/edit. Style outline blanc
pour ne pas concurrencer visuellement le rouge de Supprimer.

Cohérent avec les autres tableaux admin où le clic sur le titre ouvre
maintenant l'édition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 13:55:19 +02:00
2026-05-26 22:29:57 +02:00

Le Donut Infolab — API

API JSON pour Le Donut Infolab, plateforme de projets citoyens marseillais structurée autour de la vision Donut (plancher social / plafond écologique).

  • Stack : PHP 8.3 + MariaDB 11.4 (compatible MySQL 8.x pour le dev local).
  • Routing : router maison, pas de framework, autoload PSR-4 sans Composer.
  • Auth : Bearer token unique (ADMIN_TOKEN) sur toutes les écritures. Lectures publiques.
  • Front : séparé (React/Vue), pas dans ce repo.

Sous-domaine cible en prod : https://infolab.ledonut-marseille.org


Pré-requis

Prod (serveur Donut Marseille)

  • Docker (la stack y tourne déjà : donut-mariadb, donut-nextcloud, etc.)
  • Caddy natif côté hôte
  • Réseau Docker externe donut-net (déjà créé)
  • Conteneur donut-mariadb opérationnel et accessible sur ce réseau

Dev local (Laragon, Windows)

  • PHP 8.3+ (Laragon embarque PHP 8.3.30)
  • MariaDB ou MySQL 8.x (Laragon embarque MySQL 8.4)
  • Apache (Laragon) avec mod_rewrite (activé par défaut)
  • bash, curl, openssl pour les scripts (Git Bash sous Windows)

Démarrage rapide en dev local (Laragon)

1. Créer la base de données et l'utilisateur applicatif

Dans une console Laragon (ou mysql -uroot) :

CREATE DATABASE infolab CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'infolab'@'localhost' IDENTIFIED BY 'dev-pass-change-me';
GRANT ALL PRIVILEGES ON infolab.* TO 'infolab'@'localhost';
FLUSH PRIVILEGES;

2. Appliquer les migrations

⚠️ Important sous Windows : passer --default-character-set=utf8mb4 au client mysql. Le client Windows utilise par défaut cp850 comme charset de session, ce qui mangle les bytes UTF-8 du fichier seed avant insertion (Démocratie devient D├®mocratie en base). Le flag force le client à passer les bytes intacts.

mysql --default-character-set=utf8mb4 -uinfolab -p'dev-pass-change-me' infolab \
    < migrations/001_init_schema.sql
mysql --default-character-set=utf8mb4 -uinfolab -p'dev-pass-change-me' infolab \
    < migrations/002_seed_categories.sql

# Vérifier (les bytes de "é" doivent être C3 A9, pas E2 94 9C C2 AE) :
mysql -uinfolab -p'dev-pass-change-me' -Nse \
    "SELECT HEX(name) FROM categories WHERE slug='democratie-citoyennete'" infolab
# Attendu : 44C3A96D6F6372617469652026204369746F79656E6E6574C3A9

Le script migrations/apply.sh est conçu pour la prod (Docker) ; le client mariadb du conteneur utilise déjà utf8mb4 par défaut, donc pas de souci là-bas.

3. Créer un fichier .env (gitignoré)

cp .env.example .env

Puis éditer .env :

DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=infolab
DB_USER=infolab
DB_PASS=dev-pass-change-me

ADMIN_TOKEN=$(openssl rand -hex 32)   # ← remplacer par la valeur générée

CORS_ORIGIN=*
APP_ENV=development
APP_DEBUG=true

4. Lancer Laragon

Laragon détecte automatiquement le dossier public/ du projet et expose l'URL http://donut-gestprojet.test (vhost auto).

Vérifier :

curl http://donut-gestprojet.test/api/health
# {"status":"ok","time":"2026-05-28T..."}

5. Lancer la suite de smoke tests

ADMIN_TOKEN="$(grep ^ADMIN_TOKEN= .env | cut -d= -f2-)" \
  INFOLAB_URL=http://donut-gestprojet.test \
  ./tests/smoke.sh

Endpoints

Tous préfixés par /api. Format de réponse : application/json; charset=utf-8.

Conventions

  • Listes paginées : { "data": [...], "meta": { page, per_page, total, total_pages } }
  • Listes non paginées : { "data": [...] }
  • Détail d'une ressource : objet JSON directement
  • Erreurs uniformes : { "error": "code", "message"?: "...", "fields"?: {...} }

Codes HTTP

Code Quand Body
200 Lecture ou update OK l'objet
201 Création OK l'objet créé
204 Suppression OK (vide)
400 JSON invalide ou param connu mais mal formé {error:"bad_request",message:...}
401 Token absent / faux sur une route protégée {error:"unauthorized"}
404 Ressource inexistante {error:"not_found"}
405 Bonne URL, mauvaise méthode {error:"method_not_allowed",allowed:[...]}
409 Conflit (slug pris, FK bloquée, déjà membre) {error:"conflict",message:...}
422 Payload invalide {error:"validation_failed",fields:{...}}
500 Bug imprévu — voir Dozzle {error:"internal_server_error"}

Lectures publiques (pas de token)

Méthode + Path Description
GET /api/health Sonde de vie + ping BDD
GET /api/categories 6 catégories triées par sort_order
GET /api/categories/:slug Détail + projects_count
GET /api/members?page=1&per_page=20 Annuaire paginé
GET /api/members/:slug Détail + projets auxquels le membre participe
GET /api/projects?category=&status=&q=&page=&per_page= Liste paginée
GET /api/projects/:slug Détail complet (catégorie, équipe, roadmap, compteurs)
GET /api/projects/:slug/roadmap Étapes triées par sort_order
GET /api/projects/:slug/members Équipe avec rôle
GET /api/projects/:slug/discussions Messages racines + réponses imbriquées
GET /api/projects/:slug/resources Ressources triées par sort_order
GET /api/activity?limit=20&project=slug Flux d'activité
GET /api/stats KPIs du dashboard

Écritures protégées (Authorization: Bearer <ADMIN_TOKEN>)

Méthode + Path Notes
POST /api/categories Validation : name requis
PATCH /api/categories/:slug Partial update
DELETE /api/categories/:slug 409 si projets attachés
POST /api/members Validation : display_name requis ; initiales auto-générées
PATCH /api/members/:slug Partial update
DELETE /api/members/:slug Cascade : retrait des projets, discussions/activity nullifiées
POST /api/projects Log created_project
PATCH /api/projects/:slug Log updated_project (+ completed_project si transition status=completed)
DELETE /api/projects/:slug Soft delete (set deleted_at)
POST /api/projects/:slug/members {member_slug, role} ; log joined ; 409 si déjà membre
DELETE /api/projects/:slug/members/:member_slug Log left
POST /api/projects/:slug/roadmap Log added_step
PATCH /api/projects/:slug/roadmap/:id Log completed_step si is_done passe à true
DELETE /api/projects/:slug/roadmap/:id
POST /api/projects/:slug/discussions {author_name, body, parent_id?, author_member_id?} ; log posted_discussion
PATCH /api/projects/:slug/discussions/:id
DELETE /api/projects/:slug/discussions/:id Cascade des réponses
POST /api/projects/:slug/resources {title, url, kind?} ; log added_resource
PATCH /api/projects/:slug/resources/:id
DELETE /api/projects/:slug/resources/:id

Exemples curl

URL=http://donut-gestprojet.test
TOKEN=xxxx-votre-token

# Lecture publique
curl -s "$URL/api/categories" | head
curl -s "$URL/api/projects?category=education-ecoles&status=active&q=ecole"
curl -s "$URL/api/stats"

# Création (token requis)
curl -s -X POST "$URL/api/members" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"display_name":"Jean Dupont","kind":"person","contact_email":"jean@example.org"}'

curl -s -X POST "$URL/api/projects" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"title":"Cartographie des îlots de chaleur","category_id":5,"status":"active","progress_percent":10}'

# Patch
curl -s -X PATCH "$URL/api/projects/cartographie-des-ilots-de-chaleur" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"status":"completed","progress_percent":100}'

# Suppression (soft)
curl -s -X DELETE "$URL/api/projects/cartographie-des-ilots-de-chaleur" \
  -H "Authorization: Bearer $TOKEN"

Frontend (SPA Vue 3)

Le SPA vit dans /frontend/ — Vue 3 + Vite + TypeScript + Tailwind v4 + Vue Router. Il consomme l'API JSON et propose les pages : Tableau de bord, Vision Donut (viz SVG custom), Projets, Catégories, Fiche projet, Nouveau projet, Archives, Connexion admin.

Dev local

# Backend PHP : Laragon sert déjà /api/* sur http://donut-gestprojet.test
cd frontend
npm install
npm run dev          # http://localhost:5173 (Vite proxy /api vers Laragon)

Build de prod

cd frontend
npm run build        # vue-tsc + vite build -> frontend/dist/

Le SPA est servi par Caddy en statique depuis frontend/dist/, l'API par le conteneur PHP. Détails dans DEPLOY.md.

Architecture

  • Aucune lib UI — composants maison + Tailwind v4 (utilities classes).
  • Pas de Pinia : la source de vérité est l'API. État local via ref/reactive.
  • HTTP : fetch natif + petit wrapper src/api/client.ts (typage + erreurs).
  • Auth admin : token Bearer stocké dans localStorage, header injecté sur tous les writes. Validé "best-effort" au login en pingant POST /api/members avec body vide.
  • Routing : Vue Router 4 en mode history, code-split par page (lazy import).
  • Donut viz : SVG natif, géométrie polaire calculée à la main dans DonutPage.vue.

Ajouter une nouvelle entité

  1. Écrire la migration SQL dans migrations/00X_xxx.sql (CREATE TABLE).
  2. Créer le contrôleur dans src/Controllers/XxxController.php (un par entité, méthodes index/show/store/update/destroy).
  3. Brancher les routes dans public/index.php (les GET sont publiques, POST/PATCH/DELETE sont protégés automatiquement par le router).
  4. Suivre les patterns existants :
    • Tous les accès SQL via Database::run/fetchAll/... (prepared statements).
    • Validation via new Validator($body) ; finir par $v->check().
    • Réponses via Response::json(...) / Response::noContent().
    • Erreurs métier via throw new \ApiException($status, $payload) ou api_abort(...).
  5. Ajouter quelques curl dans tests/smoke.sh pour couvrir.

Choix d'implémentation à connaître

  • Sluggification : Slugger::slugify utilise intl/Transliterator si dispo (translittération propre des accents), avec fallback bas niveau. Unicité garantie par Slugger::unique (suffixe -2, -3...).
  • Activité : ActivityLogger::log est best-effort — un échec d'écriture ne casse pas la requête en cours, juste un error_log vers stderr.
  • Soft delete : seuls les projets ont deleted_at. Les autres entités sont supprimées en dur, avec cascades définies au schéma.
  • Stats : participants_total = nombre total de membres dans l'annuaire (interprétation choisie : "annuaire des contributeurs" = participants potentiels). Si on veut "participants actifs sur un projet", il faudrait un COUNT(DISTINCT member_id) FROM project_members.
  • CORS : Access-Control-Allow-Origin configurable via CORS_ORIGIN. Par défaut * en V1. À restreindre en V2 à l'origine du front.
  • Pagination : per_page capé à 100 côté serveur.
  • JSON typé : PDO::ATTR_STRINGIFY_FETCHES=false + casts explicites dans les present() -> les int/bool sont propres dans la réponse, pas des strings.

Roadmap V2

  • Auth réelle : OIDC via Nextcloud (nextcloud.ledonut-marseille.org), fin du Bearer admin partagé. Rôles utilisateur, modération par projet.
  • Uploads d'images : cover_image_url géré côté serveur (drag & drop vers /srv/donut/web/infolab/uploads/ avec MIME check + redimensionnement).
  • Notifications email : transactional emails sur joined / posted_discussion via Postfix sortant (relais SMTP existant sur le serveur).
  • Webhooks : notifier le bot Mattermost du Donut sur création/clôture de projet.
  • Snapshots de l'activity_log : compaction mensuelle pour éviter l'inflation indéfinie de la table.

Arborescence

.
├── README.md                 # ce fichier
├── DEPLOY.md                 # déploiement serveur Donut (étape par étape)
├── .env.example              # template secrets
├── .gitignore                # cf. fichier (couvre .env, vendor, etc.)
├── docker-compose.yml        # cible prod : conteneur donut-infolab sur donut-net
├── Dockerfile                # php:8.3-apache + pdo_mysql + intl + opcache
├── caddy.conf                # vhost infolab.ledonut-marseille.org
├── public/
│   ├── index.php             # front controller (toutes les routes)
│   └── .htaccess             # rewrite vers index.php (Apache/Laragon)
├── src/
│   ├── bootstrap.php         # env, autoload PSR-4, ApiException, handlers
│   ├── Database.php          # singleton PDO + helpers fetchOne/All/insert/...
│   ├── Router.php            # routing :params + flag auth par route
│   ├── Request.php           # parse JSON body, headers, query
│   ├── Response.php          # json/error/noContent + CORS
│   ├── Auth.php              # checkAdminToken (Bearer)
│   ├── Validator.php         # règles fluentes -> 422 fields:{...}
│   ├── Slugger.php           # slugify + unicité
│   ├── ActivityLogger.php    # insert activity_log (best-effort)
│   └── Controllers/
│       ├── HealthController.php
│       ├── CategoriesController.php
│       ├── MembersController.php
│       ├── ProjectsController.php
│       ├── ProjectMembersController.php
│       ├── RoadmapController.php
│       ├── DiscussionsController.php
│       ├── ResourcesController.php
│       ├── ActivityController.php
│       └── StatsController.php
├── migrations/
│   ├── 001_init_schema.sql
│   ├── 002_seed_categories.sql
│   └── apply.sh              # idempotent, suivi via schema_migrations
└── tests/
    └── smoke.sh              # appelle les endpoints clés, exit non-zéro si KO
S
Description
No description provided
Readme 1.7 MiB
Languages
Vue 55.9%
PHP 27.5%
Shell 10.5%
TypeScript 5.1%
CSS 0.6%
Other 0.4%