Arnaud 2b7d55581f feat(admin): clic sur titre de projet -> écran d'édition
Cohérence finale des 3 tableaux admin : le clic sur l'élément principal
de la 1re colonne pointe maintenant sur l'écran d'édition admin
correspondant pour les projets, les membres et les catégories.

Avant : project-detail (fiche publique). Maintenant : admin-project-edit.
Le lien "Éditer" en bout de ligne reste pour la découvrabilité, et la
fiche publique reste accessible depuis la nav globale ou /projects.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 13:53:37 +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%