# API d'administration

> Les routes du port d'administration de la passerelle - sondes, configuration en vigueur, simulation, journaux, métriques - avec un appel et sa réponse.

Référence - Depuis la version 0.1.0 - Flexway v1.7.1 - https://flexway.io/docs/reference-admin-api/

La passerelle sert son administration sur un **port à elle**, `:9090` par défaut
(`FLEXWAY_ADMIN_ADDR`), jamais sur le port du trafic. Le contrat complet de cette surface est publié
avec la documentation, au format OpenAPI 3.1 (`admin.openapi.yaml`) ; cette page en donne l'usage.

> [!CAUTION]
> Ce port ne doit **jamais** être routable de l'extérieur : `/config` liste les émetteurs et les amonts,
> `/errors` les erreurs de tous les tenants, `/simulate` décrit chaque décision. Ne l'ouvrez qu'à
> l'orchestrateur, au collecteur de métriques et à la console. En Kubernetes, le chart lui donne son
> propre `Service` `ClusterIP` et une `NetworkPolicy` activée par défaut.

| Route | Rôle | Depuis |
| --- | --- | --- |
| [`GET /healthz`](#get-healthz) | Vivacité du processus | `0.1.0` |
| [`GET /readyz`](#get-readyz) | Prêt à servir | `0.1.0` |
| [`GET /config`](#get-config) | Configuration en vigueur, secrets masqués | `0.1.0` |
| [`GET /metrics`](#get-metrics) | Métriques au format Prometheus | `0.2.0` |
| [`POST /simulate`](#post-simulate) | Décision sur une requête décrite, sans l'amont | `1.0.0` |
| [`POST /simulate/mqtt`](#post-simulatemqtt) | Décision de l'écouteur MQTT sur un paquet décrit, sans le courtier | `1.7.0` |
| [`GET /events`](#get-events) | Journal des évènements de la réplique | `1.2.0` |
| [`GET /errors`](#get-errors) | Journal des erreurs récentes de la réplique | `1.3.0` |
| [`GET /sparkplug/nodes`](#get-sparkplugnodes) | Table des nœuds Sparkplug B de la réplique | `1.7.0` |

## GET /healthz

Le processus est vivant - jamais un avis sur sa configuration : un processus vivant mais pas prêt doit
être laissé tranquille, pas redémarré en boucle. Toujours `200`.

```bash
curl -s http://localhost:9090/healthz
# {"status":"ok","uptime_sec":42}
```

## GET /readyz

Une configuration est chargée et servie. `200` avec le nombre d'opérations ; `503` avec la raison avant
le premier chargement et dès le début d'un arrêt (la réplique sort de la rotation avant de drainer).

```bash
curl -s http://localhost:9090/readyz
# 200 {"status":"ready","operations":3}
# 503 {"status":"not ready","reason":"no snapshot loaded yet"}
```

## GET /config

Chaque clé de `flexway.yaml` telle que chargée, les valeurs secrètes masquées, plus ce que la passerelle
en déduit : `build` (version et commit), `config_version`, `config_digest`, `last_reload`, `catalog`
(les opérations servies), `chains` (la chaîne de politiques de chaque opération et sa provenance) et,
quand ils sont configurés, `agents`, `kafka`, `ai`, `a2a`, `mcp_federation`. `503 not_ready` tant
qu'aucune configuration n'est chargée.

```bash
curl -s http://localhost:9090/config | jq '{build, config_digest, last_reload}'
```

## GET /metrics

L'exposition Prometheus des métriques de la passerelle (`text/plain`). Leurs noms et dimensions sont une
surface stable depuis `1.0.0` : voir [Observer](https://flexway.io/docs/operations-observability/).

```bash
curl -s http://localhost:9090/metrics | grep '^flexway_'
```

## POST /simulate

Ce que la passerelle ferait d'une requête **décrite** : routage, origine, autorisation contre l'identité
décrite (aucun jeton n'est vérifié), liste blanche, validation, chaîne de politiques - puis arrêt avant
l'amont : aucun jeton échangé, aucun seau ni quota débité, rien compté. `method`, `host` et `path` sont
obligatoires ; `headers` et `identity` (`issuer`, `subject`, `tenant`, `scopes`, `roles`) facultatifs.

```bash
curl -s http://localhost:9090/simulate -d '{"method":"GET","host":"api.example.com","path":"/orders?status=open",
  "identity":{"issuer":"customers","subject":"alice","tenant":"acme","scopes":["orders:read"]}}'
```

`200` avec `verdict` (`served` ou `refused`), `status`, `code` d'un refus, `operation`, `upstream`,
`chain`, les politiques appliquées et la requête sortante quand elle est servie ; `400
malformed_request` si la description est incomplète.

## POST /simulate/mqtt

Ce que l'écouteur MQTT des appareils ferait d'un paquet **décrit** (`publish`, `subscribe` ou `will`, le
testament d'un `CONNECT`) d'un appareil décrit (`identity` : `kind` `token`, `api_key` ou `certificate`,
`id` d'une clé ou d'un client `auth.mtls`, `issuer`, `subject`, `tenant`, `scopes`, `claims`, champs du
certificat) : opérations que l'identité satisfait, canaux aux valeurs liées écrites, jokers contenus,
groupes `$share/`, QoS, Response Topic - puis arrêt avant le courtier : rien n'est ouvert, ni débit ni
quota débité, rien compté. `packet`, `topic` et `identity` sont obligatoires. `404` sans écouteur MQTT.

```bash
curl -s http://localhost:9090/simulate/mqtt -d '{"packet":"publish","topic":"plant/acme/press-07/telemetry","qos":1,
  "identity":{"kind":"api_key","id":"press-07"}}'
```

`200` avec `verdict` (`served` ou `refused`), `reason_code` et `cause` d'un refus, `operation`,
`upstream`, le topic ou filtre `written` vers le courtier, la `qos` qu'il voit et le `client_id` lié ;
`400 malformed_request` si la description est incomplète.

## GET /events

Ce qui s'est passé sur **la réplique qui répond** : rechargements (`reload`), transitions des
coupe-circuits (`breaker`), délestage (`shedding`), chaînes d'agents refusées (`delegation`). Les 128
derniers évènements de chaque sorte, du plus récent au plus ancien, en mémoire - perdus au redémarrage.
Jamais un tenant ni rien de ce qu'un appelant a envoyé.

```bash
curl -s http://localhost:9090/events | jq '.events[0]'
# {"at":"...","kind":"reload","result":"loaded","version":"7fbb3dc21e1b"}
```

## GET /errors

Les erreurs récentes de la réplique qui répond, en trois anneaux de 256 (`upstream`, `refused`,
`internal`), du plus récent au plus ancien, au plus 20 écritures par seconde et par sorte (au-delà,
l'erreur est comptée dans `dropped` de sa sorte). Chaque entrée porte l'opération, l'amont, le tenant
vérifié, le statut, le code, l'étape, la cause, la durée et la trace quand elle est échantillonnée ;
jamais le chemin reçu, les en-têtes ni le corps.

```bash
curl -s http://localhost:9090/errors | jq '.errors[0]'
```

## GET /sparkplug/nodes

La table des nœuds Sparkplug B et des applications hôtes qui traversent la réplique qui répond : état (`online`,
`offline`, `moved`, `unknown`), `bdSeq`, écarts, nombre d'appareils (`devices_online`, `devices_total`), commandes
admises et refusées ; `?tenant=` filtre un tenant, `?tenant=&node=` rend ce seul nœud avec la liste de ses appareils.
La liste ne porte jamais les appareils : au plus `max_nodes` entrées aux champs bornés (un identifiant fait 255
octets au plus).
Bornée par `observability.sparkplug`, perdue au redémarrage ; `404` sans écouteur MQTT. Les noms de nœuds n'existent
que là, jamais dans une métrique ([Sparkplug B et OPC UA PubSub JSON](https://flexway.io/docs/operations-sparkplug/#la-table-des-n%C5%93uds)).

```bash
curl -s 'http://localhost:9090/sparkplug/nodes?tenant=acme' | jq '.nodes[0].state'
```

## Route inconnue

Toute autre route répond `404` en `application/problem+json` (RFC 9457), comme toute erreur de cette
surface :

```json
{
  "type": "https://flexway.dev/problems/not_found",
  "title": "No operation is declared for this route",
  "status": 404,
  "detail": "No such admin route.",
  "instance": "/debug/pprof/",
  "code": "not_found"
}
```
