3a32e93ab1
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>
57 lines
1.9 KiB
PHP
57 lines
1.9 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Infolab\Controllers;
|
|
|
|
use Infolab\Database;
|
|
use Infolab\Request;
|
|
use Infolab\Response;
|
|
|
|
/**
|
|
* GET /api/activity — flux pour le dashboard ("Vous a rejoint le projet X").
|
|
* Filtres : ?limit=N (1..100, défaut 20), ?project=slug.
|
|
*/
|
|
final class ActivityController
|
|
{
|
|
public function index(Request $req, array $params): void
|
|
{
|
|
$limit = max(1, min(100, (int) $req->query('limit', 20)));
|
|
|
|
$where = '1=1';
|
|
$args = [];
|
|
|
|
$projectSlug = $req->query('project');
|
|
if ($projectSlug !== null && $projectSlug !== '') {
|
|
$pid = Database::fetchValue('SELECT id FROM projects WHERE slug = :s', [':s' => (string) $projectSlug]);
|
|
if ($pid === null) {
|
|
// Projet inconnu : on renvoie un flux vide (pas d'erreur, le slug peut
|
|
// simplement ne pas exister côté serveur — comportement choisi pour
|
|
// simplifier le widget côté front).
|
|
Response::json(['data' => []]);
|
|
return;
|
|
}
|
|
$where = 'project_id = :pid';
|
|
$args[':pid'] = (int) $pid;
|
|
}
|
|
|
|
$rows = Database::fetchAll(
|
|
"SELECT id, project_id, member_id, actor_label, verb, summary, occurred_at
|
|
FROM activity_log WHERE $where
|
|
ORDER BY occurred_at DESC, id DESC LIMIT $limit",
|
|
$args,
|
|
);
|
|
|
|
$data = array_map(static fn(array $r): array => [
|
|
'id' => (int) $r['id'],
|
|
'project_id' => $r['project_id'] === null ? null : (int) $r['project_id'],
|
|
'member_id' => $r['member_id'] === null ? null : (int) $r['member_id'],
|
|
'actor_label' => $r['actor_label'],
|
|
'verb' => $r['verb'],
|
|
'summary' => $r['summary'],
|
|
'occurred_at' => $r['occurred_at'],
|
|
], $rows);
|
|
|
|
Response::json(['data' => $data]);
|
|
}
|
|
}
|