# API

Base publique : `https://fort-fort-lointain.festyggdrasil.fr/api/v1`

## Brancher une autre application

Les adresses ci-dessous sont **publiques** : aucune clé, aucun en-tête
particulier. Elles servent à ce qu'un autre site de l'écosystème Yggdrasil
affiche les mêmes informations que celui-ci, sans jamais les ressaisir.

| Adresse | Ce qu'elle donne | Rythme conseillé |
|---|---|---|
| `GET /infos` | Les informations pratiques (dates, lieu, accès, parking, paiement, accessibilité, billetterie) | 1 fois par heure |
| `GET /faq` | Les questions/réponses de la base de connaissances, les plus consultées en tête | 1 fois par heure |
| `GET /site` | La configuration publique : billetterie, logo, réseaux sociaux, liens du menu Pro, partenaires | 1 fois par heure |
| `GET /activities` | Les animations et troupes (cache de La Comte) | 1 fois par heure |
| `GET /exposants` | Les créateurs et exposants (cache d'Orgexpo) | 1 fois par heure |
| `GET /photographes` | Les photographes accrédités | 1 fois par jour |
| `GET /planning` | Le programme des scènes (vide avant sa date de diffusion) | 1 fois par heure |
| `GET /plan` | Le plan du site (vide avant sa date de dévoilement) | 1 fois par heure |

### Les trois règles à respecter

1. **Appelez depuis VOTRE SERVEUR, jamais depuis le navigateur du
   visiteur.** Ces contenus doivent se trouver dans le HTML de votre page
   pour être lus par les moteurs de recherche : « horaires », « accès »,
   « parking » sont exactement ce que les gens cherchent.
   (Un appel navigateur reste possible pour un usage annexe, mais votre
   domaine doit alors figurer dans `CHAT_ALLOWED_ORIGINS` ici.)
2. **Gardez une copie locale.** Votre site doit continuer d'afficher les
   informations si celui-ci est en panne ou change d'adresse.
3. **Ne recopiez pas les textes à la main.** La source de vérité est le
   back-office d'ici : corrigé ici, corrigé partout.

### `GET /infos` : les informations pratiques

```json
{
  "source": "https://fort-fort-lointain.festyggdrasil.fr",
  "maj": "2026-08-27 07:16:55",
  "items": [
    {
      "icone": "calendar_month",
      "titre": "Dates & horaires",
      "texte": "Samedi 5 septembre : 10h à 19h.\nDimanche 6 septembre : 10h à 18h.",
      "lien_libelle": null,
      "lien_url": null
    },
    {
      "icone": "confirmation_number",
      "titre": "Billetterie",
      "texte": "Billets en ligne ou sur place le jour J.",
      "lien_libelle": "Réserver maintenant",
      "lien_url": "https://www.billetweb.fr/…"
    }
  ]
}
```

- `icone` : un nom d'icône **Material Symbols** (`calendar_month`,
  `location_on`, `tram`, `local_parking`, `restaurant`, `payments`,
  `accessible`…). Libre à vous de l'afficher ou d'utiliser la vôtre.
- `texte` : les retours à la ligne comptent (`nl2br` à l'affichage).
- `lien_url` : déjà résolu. Une carte qui pointe « la billetterie » vous
  donne l'adresse réelle du moment : rien à deviner de votre côté.
- Les cartes arrivent **dans l'ordre d'affichage**, les dépubliées en moins.

### Connecteur prêt à coller (site Laravel)

```php
// app/Services/InfosPratiques.php
class InfosPratiques
{
    private const SOURCE = 'https://fort-fort-lointain.festyggdrasil.fr/api/v1/infos';

    /** Rapatrie et met en cache. À appeler par le cron, une fois par heure. */
    public function importer(): int
    {
        $res = Http::timeout(15)->acceptJson()->get(self::SOURCE);
        if (! $res->successful()) {
            return 0; // on garde la copie précédente : jamais de page vide
        }

        Setting::put('infos_pratiques', (string) json_encode($res->json('items') ?? []));
        Setting::put('infos_pratiques_maj', now()->toDateTimeString());

        return count($res->json('items') ?? []);
    }

    /** La copie locale, pour l'affichage (jamais d'appel réseau ici). */
    public function cartes(): array
    {
        return json_decode((string) Setting::get('infos_pratiques', '[]'), true) ?: [];
    }
}
```

```blade
{{-- Rendu côté serveur : le texte est dans la page, donc référençable. --}}
@foreach($cartes as $c)
  <div class="card">
    <h3><span class="msym">{{ $c['icone'] ?: 'info' }}</span> {{ $c['titre'] }}</h3>
    <p>{!! nl2br(e($c['texte'])) !!}</p>
    @if($c['lien_libelle'] && $c['lien_url'])
      <p><a class="btn" href="{{ $c['lien_url'] }}">{{ $c['lien_libelle'] }}</a></p>
    @endif
  </div>
@endforeach
```

### Connecteur prêt à coller (site en PHP simple)

```php
$cache = __DIR__.'/cache/infos.json';

// Rafraîchi au plus une fois par heure ; sinon on sert la copie.
if (! is_file($cache) || time() - filemtime($cache) > 3600) {
    $json = @file_get_contents('https://fort-fort-lointain.festyggdrasil.fr/api/v1/infos');
    if ($json) { @mkdir(dirname($cache), 0775, true); file_put_contents($cache, $json); }
}

$items = json_decode((string) @file_get_contents($cache), true)['items'] ?? [];
foreach ($items as $c) {
    echo '<div class="card"><h3>'.htmlspecialchars($c['titre']).'</h3><p>'
        .nl2br(htmlspecialchars($c['texte'])).'</p></div>';
}
```

## Publique : Chatbot (`/api/v1/chat`)

CORS en whitelist (`CHAT_ALLOWED_ORIGINS`). Rate limit : 20 req/min.

### POST `/api/v1/chat` : poser une question

```json
{ "conversation": "uuid|null", "message": "Quels sont les horaires ?" }
```

Réponse :

```json
{ "conversation": "uuid", "reply": "Le festival a lieu...", "answered": true, "need_email": false }
```

Si `answered=false`, `need_email=true` : le widget demande l'email, puis renvoie :

```json
{ "conversation": "uuid", "email": "x@y.fr", "consent": true, "question": "..." }
```

Réponse : `{ "reply": "Merci ! Votre question a été transmise...", "done": true }`

### GET `/api/v1/chat/health`

`{ "status": "ok" }`

## Format d'erreur standard (cross-app)

```json
{ "error": { "code": "VALIDATION_ERROR", "message": "...", "details": { "field": "email", "rule": "required" } } }
```

> API interne (Hub, OpenAPI/Swagger) : roadmap. Voir INTEGRATIONS.md.
