Implements the JSON API for Le Donut Infolab per INSTRUCTIONS.md.
Schema (migrations/001-002):
- 8 tables: categories, members, projects, project_members, roadmap_steps,
discussions, resources, activity_log
- 6 categories seeded with colors / dimensions (social, ecologique, transversal)
- FK contraintes + soft delete sur projects, CHECK sur progress_percent
Bare PHP framework (src/):
- Front controller public/index.php + router :params + auth flag par route
- PSR-4 autoload sans Composer (bootstrap.php)
- Database PDO singleton + helpers fetchOne/All/insert/update/delete
- Validator fluent -> 422 {error: validation_failed, fields: {...}}
- Bearer token admin (hash_equals) sur POST/PATCH/DELETE
- Slugger avec unicité (suffixe -2, -3...) via intl Transliterator
- ActivityLogger best-effort sur activity_log
- Erreurs uniformes : 400/401/404/405/409/422/500 + CORS configurable
10 contrôleurs (Health, Categories, Members, Projects, ProjectMembers,
Roadmap, Discussions, Resources, Activity, Stats). Transitions de statut
loggées (created_project, completed_step, joined, etc.).
Cible prod :
- Dockerfile (php:8.3-apache + pdo_mysql + intl + opcache)
- docker-compose.yml sur réseau donut-net, bind 127.0.0.1:8003
- caddy.conf pour infolab.ledonut-marseille.org
- migrations/apply.sh idempotent (table schema_migrations)
- DEPLOY.md avec commandes SSH exactes
Tests :
- tests/smoke.sh exerce 10 endpoints clés, body envoyé via stdin
(--data-binary @-) pour gérer l UTF-8 sous Git Bash Windows.
Vérifié localement sous Laragon Apache + MySQL 8.4 : 10/10 OK + cas
négatifs (401, 404, 405, 409, 422) conformes au contrat.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
25 KiB
Prompt Claude Code — Application Le Donut Infolab
Copie tout ce qui suit (entre les deux lignes de séparation) dans Claude Code, depuis un dossier vide où tu veux que le projet soit créé. Tu peux aussi le sauver dans un fichier
INSTRUCTIONS.mdet lancerclaudedans le dossier, puis dire « lis INSTRUCTIONS.md et exécute ».
Mission
Tu vas générer une application PHP 8.3 + MariaDB complète appelée Le Donut Infolab : une plateforme de projets citoyens marseillais, structurée autour de la vision Donut (plancher social / plafond écologique).
Le backend expose une API JSON pure. Le front-end (React/Vue) est séparé et ne fait pas partie de cette mission — tu produis uniquement l'API et son admin par token.
L'application sera déployée sur le serveur auto-hébergé Le Donut Marseille (Ubuntu 26.04, Docker, MariaDB 11.4, Caddy en reverse proxy) sous le sous-domaine infolab.ledonut-marseille.org.
Contexte serveur cible (à respecter)
- MariaDB existante : conteneur
donut-mariadbsur le réseau Dockerdonut-net(IPv4172.30.0.0/16+ IPv6fd00:d07c::/64). Port3306interne uniquement, pas exposé sur l'hôte. - Caddy natif sur l'hôte (pas en conteneur). Les services sont reverse-proxy via des fichiers
caddy.confposés dans/srv/donut/compose/<service>/caddy.confet importés par/etc/caddy/Caddyfile. - Convention de nommage des conteneurs :
donut-<service>(ex:donut-infolab). - Arborescence cible sur le serveur :
/srv/donut/compose/infolab/→docker-compose.yml,Dockerfile,caddy.conf/srv/donut/web/infolab/→ code PHP (bind-mount dans le conteneur)/srv/donut/secrets/infolab.env→ secrets (mode 640, root:donut-admins)
- Conteneur PHP : doit tourner en UID 33 (
www-data) pour cohérence avec les autres services Apache de la stack. - Pas de port exposé sur l'hôte sauf 127.0.0.1 : on bind sur
127.0.0.1:8003:80(les ports 8001 et 8002 sont déjà pris par WordPress et Nextcloud).
Choix techniques validés (ne pas remettre en question)
- PHP 8.3 pur, procédural/POO simple, router maison. Pas de Composer si possible, ou Composer uniquement pour l'autoload PSR-4 — pas de framework.
- API JSON pure (Content-Type: application/json partout, codes HTTP RESTful).
- Pas d'auth utilisateur en V1. Admin caché par Bearer token (
Authorization: Bearer <ADMIN_TOKEN>) requis pour toutes les routes POST / PUT / PATCH / DELETE. Les GET sont publiques. - PDO + requêtes préparées partout. Jamais de concaténation SQL.
- MariaDB 11.4 existante, charset
utf8mb4/utf8mb4_unicode_ci.
Schéma de base de données
Le schéma est déjà rédigé. Tu vas le créer dans migrations/001_init_schema.sql et migrations/002_seed_categories.sql. Voici le contenu exact à utiliser :
migrations/001_init_schema.sql
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- categories : 6 catégories thématiques de la maquette
DROP TABLE IF EXISTS categories;
CREATE TABLE categories (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
slug VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
description TEXT NULL,
color_hex CHAR(7) NOT NULL DEFAULT '#888888',
icon VARCHAR(32) NULL,
dimension ENUM('social', 'ecologique', 'transversal') NOT NULL DEFAULT 'transversal',
sort_order INT UNSIGNED NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_categories_slug (slug)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- members : annuaire des contributeurs (pas une table d'auth)
DROP TABLE IF EXISTS members;
CREATE TABLE members (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
slug VARCHAR(96) NOT NULL,
display_name VARCHAR(128) NOT NULL,
initials VARCHAR(8) NULL,
kind ENUM('person', 'collective', 'org') NOT NULL DEFAULT 'person',
bio TEXT NULL,
avatar_url VARCHAR(512) NULL,
website_url VARCHAR(512) NULL,
contact_email VARCHAR(255) NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_members_slug (slug)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- projects : entité centrale
DROP TABLE IF EXISTS projects;
CREATE TABLE projects (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
slug VARCHAR(160) NOT NULL,
title VARCHAR(200) NOT NULL,
category_id BIGINT UNSIGNED NOT NULL,
status ENUM('active', 'completed', 'archived') NOT NULL DEFAULT 'active',
territory VARCHAR(160) NOT NULL DEFAULT 'Marseille',
short_description TEXT NULL,
context_description LONGTEXT NULL,
objectives LONGTEXT NULL,
cover_image_url VARCHAR(512) NULL,
external_url VARCHAR(512) NULL,
progress_percent TINYINT UNSIGNED NOT NULL DEFAULT 0,
started_at DATE NULL,
completed_at DATE NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_projects_slug (slug),
KEY idx_projects_category (category_id),
KEY idx_projects_status (status),
KEY idx_projects_deleted (deleted_at),
CONSTRAINT fk_projects_category FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE RESTRICT,
CONSTRAINT chk_projects_progress CHECK (progress_percent BETWEEN 0 AND 100)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- project_members : pivot N..N projet/contributeur avec rôle
DROP TABLE IF EXISTS project_members;
CREATE TABLE project_members (
project_id BIGINT UNSIGNED NOT NULL,
member_id BIGINT UNSIGNED NOT NULL,
role ENUM('admin', 'contributor', 'observer') NOT NULL DEFAULT 'contributor',
joined_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (project_id, member_id),
KEY idx_pm_member (member_id),
CONSTRAINT fk_pm_project FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
CONSTRAINT fk_pm_member FOREIGN KEY (member_id) REFERENCES members(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- roadmap_steps : feuille de route
DROP TABLE IF EXISTS roadmap_steps;
CREATE TABLE roadmap_steps (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
project_id BIGINT UNSIGNED NOT NULL,
title VARCHAR(200) NOT NULL,
description TEXT NULL,
step_date DATE NULL,
is_done BOOLEAN NOT NULL DEFAULT FALSE,
sort_order INT UNSIGNED NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_roadmap_project (project_id, sort_order),
CONSTRAINT fk_roadmap_project FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- discussions : onglet Discussions, arborescent (parent_id pour les réponses)
DROP TABLE IF EXISTS discussions;
CREATE TABLE discussions (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
project_id BIGINT UNSIGNED NOT NULL,
parent_id BIGINT UNSIGNED NULL,
author_name VARCHAR(128) NOT NULL,
author_member_id BIGINT UNSIGNED NULL,
body LONGTEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_disc_project (project_id, created_at),
KEY idx_disc_parent (parent_id),
CONSTRAINT fk_disc_project FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
CONSTRAINT fk_disc_parent FOREIGN KEY (parent_id) REFERENCES discussions(id) ON DELETE CASCADE,
CONSTRAINT fk_disc_member FOREIGN KEY (author_member_id) REFERENCES members(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- resources : onglet Ressources (liens, docs, datasets)
DROP TABLE IF EXISTS resources;
CREATE TABLE resources (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
project_id BIGINT UNSIGNED NOT NULL,
title VARCHAR(200) NOT NULL,
description TEXT NULL,
url VARCHAR(1024) NOT NULL,
kind ENUM('link', 'document', 'dataset', 'video', 'other') NOT NULL DEFAULT 'link',
sort_order INT UNSIGNED NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_res_project (project_id, sort_order),
CONSTRAINT fk_res_project FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- activity_log : flux du dashboard ("Vous a rejoint le projet X")
DROP TABLE IF EXISTS activity_log;
CREATE TABLE activity_log (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
project_id BIGINT UNSIGNED NULL,
member_id BIGINT UNSIGNED NULL,
actor_label VARCHAR(128) NOT NULL DEFAULT 'Vous',
verb ENUM('joined', 'left', 'created_project', 'updated_project',
'completed_step', 'added_step', 'posted_discussion',
'added_resource', 'completed_project') NOT NULL,
summary VARCHAR(255) NOT NULL,
occurred_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_act_project (project_id, occurred_at),
KEY idx_act_occurred (occurred_at),
CONSTRAINT fk_act_project FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
CONSTRAINT fk_act_member FOREIGN KEY (member_id) REFERENCES members(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SET FOREIGN_KEY_CHECKS = 1;
migrations/002_seed_categories.sql
INSERT INTO categories (slug, name, description, color_hex, icon, dimension, sort_order) VALUES
('data-open-data', 'Data & Open Data', 'Projets autour de la donnée ouverte, de la médiation et de la littératie des données.', '#3B82F6', 'database', 'transversal', 10),
('democratie-citoyennete','Démocratie & Citoyenneté', 'Projets favorisant la participation citoyenne, la transparence et la gouvernance ouverte.', '#EF4444', 'users', 'social', 20),
('habitat-solidarite', 'Habitat & Solidarité', 'Projets autour du logement, de la fracture numérique et de la solidarité.', '#14B8A6', 'home', 'social', 30),
('territoire-urbanisme', 'Territoire & Urbanisme', 'Projets liés à l''aménagement du territoire, la renaturation et la vie locale.', '#F97316', 'map', 'transversal', 40),
('ecologie-biodiversite', 'Écologie & Biodiversité', 'Projets liés aux enjeux écologiques, à la biodiversité et à la nature en ville.', '#22C55E', 'leaf', 'ecologique', 50),
('education-ecoles', 'Éducation & Écoles', 'Projets autour de l''école publique, de la formation et de l''éducation aux données.', '#A855F7', 'graduation-cap','social', 60);
Arborescence à générer
.
├── README.md # install + endpoints + exemples curl
├── DEPLOY.md # commandes exactes à lancer sur le serveur Donut
├── .env.example # template du fichier secrets
├── .gitignore
├── docker-compose.yml
├── Dockerfile
├── caddy.conf # vhost infolab.ledonut-marseille.org
├── public/
│ ├── index.php # front controller
│ └── .htaccess # fallback si pas Caddy (rewrite vers index.php)
├── src/
│ ├── bootstrap.php # charge env, db, error handler
│ ├── Database.php # singleton PDO + helpers (fetchOne, fetchAll, insert, update, delete)
│ ├── Router.php # router maison, supporte :param dans le path
│ ├── Request.php # parse JSON body, query, headers
│ ├── Response.php # json(), error(), noContent()
│ ├── Auth.php # checkAdminToken() depuis Authorization: Bearer
│ ├── Validator.php # validation des payloads
│ ├── Slugger.php # slugifie les titres en respectant l'unicité
│ ├── ActivityLogger.php # helper pour écrire dans activity_log
│ └── Controllers/
│ ├── HealthController.php # GET /api/health
│ ├── CategoriesController.php # CRUD catégories
│ ├── MembersController.php # CRUD membres
│ ├── ProjectsController.php # CRUD projets + filtres
│ ├── ProjectMembersController.php # ajout/retrait équipe d'un projet
│ ├── RoadmapController.php # CRUD étapes
│ ├── DiscussionsController.php # CRUD messages
│ ├── ResourcesController.php # CRUD ressources
│ ├── ActivityController.php # GET /api/activity
│ └── StatsController.php # GET /api/stats (dashboard)
├── migrations/
│ ├── 001_init_schema.sql
│ ├── 002_seed_categories.sql
│ └── apply.sh # script bash pour appliquer les .sql via docker exec
└── tests/
└── smoke.sh # appelle tous les endpoints clés avec curl
Routes API à implémenter
Toutes préfixées par /api. Format de réponse JSON systématique. Codes HTTP REST classiques.
Lectures publiques (pas de token)
GET /api/health→{ "status": "ok", "time": "..." }GET /api/categories→ liste triée parsort_orderGET /api/categories/:slug→ détail + count projetsGET /api/members→ liste, pagination?page=1&per_page=20GET /api/members/:slug→ détail + projets auxquels il/elle participeGET /api/projects→ liste avec filtres?category=slug&status=active&q=texte&page=1&per_page=20GET /api/projects/:slug→ détail complet (catégorie, membres avec rôle, roadmap, compteurs discussions/ressources)GET /api/projects/:slug/roadmap→ étapes triées parsort_orderGET /api/projects/:slug/members→ équipe avec rôlesGET /api/projects/:slug/discussions→ messages racines triés par date desc, avec leurs réponses imbriquéesGET /api/projects/:slug/resources→ ressources triées parsort_orderGET /api/activity→ 20 dernières activités,?limit=N&project=slugGET /api/stats→ KPIs dashboard :{ "projects_active": 5, "projects_completed": 10, "projects_archived": 9, "projects_total": 24, "participants_total": 32, "by_category": [{"slug":"...","name":"...","color_hex":"...","count":5}, ...], "by_dimension": {"social": 10, "ecologique": 5, "transversal": 9} }
Écritures (token Bearer requis)
POST /api/categories(rare, normalement seeded)PATCH /api/categories/:slugDELETE /api/categories/:slug(refus si projets attachés → 409)POST /api/membersPATCH /api/members/:slugDELETE /api/members/:slugPOST /api/projects→ crée + log activitycreated_projectPATCH /api/projects/:slug→ logupdated_project, si status passe àcompletedlogcompleted_projectDELETE /api/projects/:slug→ soft delete (setdeleted_at)POST /api/projects/:slug/members{ "member_slug": "...", "role": "..." }→ logjoinedDELETE /api/projects/:slug/members/:member_slug→ logleftPOST /api/projects/:slug/roadmap→ logadded_stepPATCH /api/projects/:slug/roadmap/:id→ si passage àis_done=truelogcompleted_stepDELETE /api/projects/:slug/roadmap/:idPOST /api/projects/:slug/discussions→ logposted_discussionPATCH /api/projects/:slug/discussions/:idDELETE /api/projects/:slug/discussions/:idPOST /api/projects/:slug/resources→ logadded_resourcePATCH /api/projects/:slug/resources/:idDELETE /api/projects/:slug/resources/:id
Règles techniques impératives
- Toutes les requêtes SQL passent par PDO avec paramètres nommés ou positionnels. Jamais de concaténation.
- Slugs : auto-générés depuis le titre si non fournis. Suffixe
-2,-3… en cas de collision. - Validation systématique des payloads POST/PATCH avant insert/update. Retour 422 avec détail des champs en erreur :
{ "error": "validation_failed", "fields": { "title": "required", "category_id": "not_found" } } - Erreurs uniformes :
- 400
{ "error": "bad_request", "message": "..." } - 401
{ "error": "unauthorized" }(token manquant ou invalide sur route protégée) - 404
{ "error": "not_found" } - 409
{ "error": "conflict", "message": "..." }(ex: slug pris, FK bloquée) - 422
{ "error": "validation_failed", "fields": {...} } - 500
{ "error": "internal_server_error" }(sans détail en prod)
- 400
- CORS : header
Access-Control-Allow-Originconfigurable via envCORS_ORIGIN(par défaut*pour V1). GérerOPTIONSpreflight. - Pas de PHP dans les contrôleurs : tout passe par
Response::json()avec un tableau associatif. - Pas de
var_dump,print_r,die,exitdans le code livré. - Pas de session PHP (API stateless).
- Logs d'erreurs :
error_log()versphp://stderr→ ainsi visible dans Dozzle (logs Docker centralisés sur ce serveur). - Date/heure en UTC côté DB. Les clients gèrent le fuseau.
- Réponses paginées :
{ "data": [...], "meta": { "page": 1, "per_page": 20, "total": 47, "total_pages": 3 } }
Format du fichier .env.example
# Base de donnees (le conteneur tourne sur donut-net, donc on adresse donut-mariadb par nom)
DB_HOST=donut-mariadb
DB_PORT=3306
DB_NAME=infolab
DB_USER=infolab
DB_PASS=CHANGE_ME_STRONG_PASSWORD
# Admin caché : token Bearer pour les routes d'écriture
# Générer avec : openssl rand -hex 32
ADMIN_TOKEN=CHANGE_ME_64_HEX_CHARS
# CORS — mettre l'origine du front en prod (ex: https://infolab.ledonut-marseille.org)
CORS_ORIGIN=*
# Environnement
APP_ENV=production
APP_DEBUG=false
docker-compose.yml
services:
donut-infolab:
build:
context: .
dockerfile: Dockerfile
container_name: donut-infolab
restart: unless-stopped
networks:
- donut-net
ports:
- "127.0.0.1:8003:80"
env_file:
- /srv/donut/secrets/infolab.env
volumes:
- /srv/donut/web/infolab:/var/www/html
depends_on: []
networks:
donut-net:
external: true
Dockerfile
FROM php:8.3-apache
# Extensions PHP nécessaires
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libicu-dev \
&& docker-php-ext-install -j$(nproc) pdo_mysql intl opcache \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Active mod_rewrite (pour le routing index.php) + headers (CORS)
RUN a2enmod rewrite headers
# Pointe Apache vers /public
ENV APACHE_DOCUMENT_ROOT=/var/www/html/public
RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' \
/etc/apache2/sites-available/*.conf \
/etc/apache2/apache2.conf \
/etc/apache2/conf-available/*.conf
# Config PHP minimale en prod
RUN { \
echo 'opcache.enable=1'; \
echo 'opcache.memory_consumption=128'; \
echo 'opcache.validate_timestamps=0'; \
echo 'display_errors=Off'; \
echo 'log_errors=On'; \
echo 'error_log=/dev/stderr'; \
echo 'expose_php=Off'; \
} > /usr/local/etc/php/conf.d/donut.ini
EXPOSE 80
caddy.conf (sera importé par /etc/caddy/Caddyfile sur l'hôte)
# infolab.ledonut-marseille.org — Le Donut Infolab (API + front)
infolab.ledonut-marseille.org {
encode gzip zstd
# Headers de sécurité de base
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
reverse_proxy 127.0.0.1:8003 {
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}
Script migrations/apply.sh
Doit, depuis l'hôte serveur :
- Lire le mot de passe root MariaDB depuis
/srv/donut/secrets/mariadb.env - Créer la base
infolabsi absente - Créer/MAJ le user
infolabavec mot de passe lu depuis/srv/donut/secrets/infolab.env(variableDB_PASS), droitsALLsurinfolab.* - Appliquer chaque
.sqldu dossier (sauf lui-même) dans l'ordre alphabétique, viadocker exec -i donut-mariadb mariadb -uroot -p"$ROOT_PWD" - Afficher un résumé (tables créées, lignes insérées dans
categories)
Le script doit être idempotent (re-run = OK).
tests/smoke.sh
Script bash qui appelle, dans l'ordre :
GET /api/healthGET /api/categories(vérifie qu'il y en a bien 6)POST /api/membersavec token (crée "Collectif des écoles de Marseille")POST /api/projectsavec token (crée "Nos Écoles" — exemple visible dans la maquette)POST /api/projects/nos-ecoles/membersavec token (ajoute le membre)POST /api/projects/nos-ecoles/roadmapavec token (ajoute "Création de l'outil 15min")GET /api/projects/nos-ecoles(vérifie structure complète)GET /api/statsGET /api/activity
Chaque appel affiche le code HTTP et un extrait de la réponse. Sortie non-zéro si un appel échoue.
Le token est lu depuis $ADMIN_TOKEN en variable d'env, l'hôte depuis $INFOLAB_URL (défaut http://localhost:8003).
DEPLOY.md
Document les commandes exactes à lancer en SSH sur arnaud.donut@82.66.40.209, dans cet ordre :
- Copier le projet vers
/srv/donut/compose/infolab/(sauf le sous-dossierpublic/etsrc/qui vont dans/srv/donut/web/infolab/) - Créer
/srv/donut/secrets/infolab.envdepuis.env.example, générerADMIN_TOKENetDB_PASS,chmod 640,chown root:donut-admins - Créer le user MariaDB + la base via le script
apply.sh - Appliquer les migrations
- Build + start du conteneur :
docker compose --env-file /srv/donut/secrets/infolab.env up -d --build - Copier
caddy.confvers/srv/donut/compose/infolab/caddy.conf(s'il n'y est pas déjà), valider la config Caddy, reload - Vérifier les DNS (rappel : ajouter un A record
infolabchez OVH vers82.66.40.209) - Lancer
tests/smoke.sh
README.md
Documente :
- Description courte du projet
- Pré-requis (Docker, MariaDB sur réseau
donut-net) - Commande de dev local rapide (avec un MariaDB temporaire en compose override)
- Liste des endpoints avec un exemple curl par endpoint majeur
- Comment ajouter une nouvelle entité (procédure)
- Roadmap V2 : auth réelle (Nextcloud OIDC), uploads d'images, notifications email
Contraintes finales
- Aucun appel à du JavaScript côté front — c'est une API JSON pure, le front est ailleurs.
- Code commenté en français (l'équipe est française), mais noms de variables/fonctions/classes en anglais.
- Génère vraiment tous les fichiers sur disque, ne fais pas que les décrire.
- Une fois tout généré, lance
tests/smoke.shsi tu peux mettre en place un MariaDB de dev local pour vérifier que ça tourne, sinon documente clairement comment je dois tester. - À la fin, affiche un résumé : fichiers créés, prochaines étapes côté serveur, points qui restent à valider avec l'utilisateur.
Procède étape par étape, en m'expliquant brièvement ce que tu fais à chaque grosse étape (migrations / squelette PHP / contrôleurs / docker / caddy / docs / tests). Pas besoin de me demander confirmation entre chaque, mais arrête-toi si tu rencontres une ambiguïté réelle (pas une question de style).
C'est parti.