Contrat API — Bascule d’un client vers J+RADIUS
Le bouton « migrer ce client en mode RADIUS », et ce que le gérant voit pendant que ça se passe.
📱 Dev de l’application : le briefing d’intégration pas à pas — écrans, pièges, protocole de test des 7 routes — est sur
/docs/CONTRAT-API-MIGRATION-RADIUS-APP. Ce document-ci reste la référence des champs.
Base : https://live.jmoai.net/api/v1/radius
1. Le fait qui rend cette bascule nécessaire
[!WARNING] Sur RouterOS, le hotspot consulte d’abord sa base locale et n’interroge RADIUS que si l’usager n’y figure pas.
Poser use-radius=yes en laissant les comptes locaux ne bascule donc rien : les clients
continuent de s’authentifier en local, RADIUS n’est jamais appelé, et aucune erreur n’est
levée.
Mesuré le 2026-08-29 : 8 707 comptes locaux sur le routeur de référence, et radacct à
874 lignes pour tout le parc, dont zéro authentification hotspot. Des déploiements
avaient pourtant réussi. La bascule était inerte.
Le retrait des comptes locaux n’est donc pas un nettoyage de confort : c’est l’étape qui fait la bascule.
2. Les six étapes, dont une seule est irréversible
1. inventaire on regarde avant de promettre
2. sauvegarde rapatriée ET vérifiée, sinon on s'arrête
3. import vers RADIUS et on COMPTE ce qui est arrivé
4. configuration le routeur parle au serveur, la confirmation est armée
5. retrait du local ← IRRÉVERSIBLE, exige trois preuves
6. première connexion une vraie authentification, avec le nom de l'usager
Les étapes 1 à 4 se rejouent sans dommage. La 5 exige trois preuves :
| Preuve | Comment elle est obtenue |
|---|---|
SAUVEGARDE_CONFIRMEE |
des octets rapatriés, pas un simple retour sans exception |
IMPORT_RADIUS_CONFIRME |
inventaire paginé du serveur RADIUS, complete: true |
ROUTEUR_JOIGNABLE |
l’inventaire a répondu |
Et un import incomplet bloque le retrait : des comptes non importés puis retirés, ce sont des tickets vendus qui disparaissent.
Si le retrait est refusé, la bascule s’arrête proprement : le routeur reste en double (local + RADIUS), le gérant continue de vendre, personne ne perd rien.
3. GET /admin/migration/{allocationId}/inventaire
Lecture seule. Aucune écriture, aucun engagement.
{
"allocation_id": "uuid",
"nas_id": "uuid",
"routeur": "HH40AF7YVNP",
"licence_active": true,
"tunnel_joignable": true,
"comptes_locaux": 8707,
"espace_flash_mo": 36,
"autorise": true,
"motif": null,
"motif_texte": null,
"avertissements": []
}
motif |
Sens |
|---|---|
LICENCE_RADIUS_INACTIVE |
pas de licence J+RADIUS active |
ROUTEUR_INJOIGNABLE |
le routeur ne répond pas |
INVENTAIRE_ILLISIBLE |
comptes non lisibles — ce n’est pas zéro compte |
TROP_DE_COMPTES_POUR_UNE_BASCULE_AUTOMATIQUE |
plus de 15 000 : lot par lot, pas en un clic |
ESPACE_FLASH_INSUFFISANT |
moins de 25 Mo : la sauvegarde échouerait |
comptes_locaux: null n’est pas 0. Le serveur refuse dans ce cas. Une application qui
afficherait « rien à migrer » sur un inventaire illisible enverrait un administrateur basculer
à l’aveugle un routeur qui porte huit mille comptes.
4. POST /admin/migration/{allocationId}
{ "retirer_local": true }
retirer_local: false fait tout sauf le retrait : utile pour préparer une migration, ou
laisser un routeur en double le temps d’observer.
{
"success": true,
"allocation_id": "uuid",
"nas_id": "uuid",
"statut": "ARMEMENT",
"etapes": [
{ "etape": "SAUVEGARDE", "etat": "OK", "comptes": 8707 },
{ "etape": "CONFIG", "etat": "OK" },
{ "etape": "IMPORT", "etat": "OK", "importes": 8707 },
{ "etape": "RETRAIT_LOCAL", "etat": "OK", "retires": 8706 },
{ "etape": "ARMEMENT", "etat": "OK" }
],
"retires": 8706,
"importes": 8707
}
RETRAIT_LOCAL peut valoir REFUSE avec un motif — la bascule s’est arrêtée là, et c’est
un succès partiel, pas un échec : rien n’a été détruit.
Le compte default-trial est toujours épargné : il est créé par RouterOS et son retrait
casse l’essai gratuit du hotspot.
| Code d’erreur | Sens |
|---|---|
MIGRATION_BACKUP_FAILED / MIGRATION_BACKUP_EMPTY |
sauvegarde impossible ou vide — rien n’a été touché |
MIGRATION_DEPLOY_FAILED |
configuration RADIUS refusée — rien n’a été supprimé |
MIGRATION_COUNT_FAILED |
comptage impossible — retrait annulé |
MIGRATION_REMOVE_STALLED |
le routeur ne retire plus rien — des comptes subsistent |
MIGRATION_REMOVE_UNVERIFIED |
retrait effectué mais non vérifiable |
Le retrait est vérifié par comptage avant/après, pas par le retour de la commande : un retrait partiel qui passerait pour complet laisserait des comptes locaux, et ce sont eux qui masquent RADIUS.
4 bis. POST /admin/migration/{allocationId}/retour — revenir en local
Le bouton inverse. Remet les comptes hotspot sur le routeur, puis coupe RADIUS.
{ "couper_radius": true }
couper_radius: false remet les comptes sans couper RADIUS : le routeur reste en double le
temps de vérifier que tout répond avant de trancher.
{
"success": true,
"statut": "REVENU_EN_LOCAL",
"comptes_restaures": 8706,
"sauvegarde_datee_du": "2026-08-29T20:00:00Z",
"ecart_avec_radius": {
"connu": true,
"ecart": 53,
"avertissement": "Le serveur cloud porte 53 compte(s) de plus que la sauvegarde : ce sont des tickets vendus depuis la bascule. Ils ne seront PAS restaurés sur le routeur."
},
"etapes": [
{ "etape": "LECTURE_SAUVEGARDE", "etat": "OK", "comptes": 8707, "a_restaurer": 8706 },
{ "etape": "PROFILS", "etat": "OK", "profils": 18 },
{ "etape": "COMPTES", "etat": "OK", "poses": 8706 },
{ "etape": "VERIFICATION", "etat": "OK", "comptes": 8706 },
{ "etape": "RADIUS_COUPE", "etat": "OK" }
]
}
Ce qui n’est JAMAIS rejoué
[!WARNING] Le fichier
.backupbinaire n’est jamais restauré. Il porte toute la configuration — pare-feu, adresses, routes, peers WireGuard, tunnel de gestion. Le rejouer à distance rendrait le routeur muet une fois sur deux, et on perdrait le chemin d’accès en même temps que la possibilité de réparer.
Seuls les profils et les comptes hotspot sont remis. Rien d’autre n’est touché.
L’ordre, et pourquoi il ne s’inverse pas
- Profils d’abord — un compte dont le profil manque échoue, et fait tomber son lot entier.
- Comptes par lots de 40, commandes idempotentes : rejouer après un échec partiel ne lève pas « already have such name ».
- Vérification par comptage.
use-radius=noen dernier. Couper RADIUS avant d’avoir remis les comptes laisserait le hotspot sans aucune base d’authentification — tous les clients dehors pendant la restauration. Si la restauration échoue ou reste incomplète, RADIUS n’est pas coupé : le hotspot garde une base qui répond.
L’entrée /radius n’est pas retirée : la laisser ne coûte rien et permet de rebasculer sans
tout reconfigurer.
Ce que le retour ne peut pas rattraper — à afficher
La sauvegarde date d’avant la bascule. Les tickets vendus depuis vivent dans RADIUS et ne
sont pas dans ce fichier : ecart_avec_radius.avertissement les compte. Inversement, les
tickets déjà consommés depuis y figurent encore et redeviendront utilisables.
On ne corrige pas ça automatiquement — toute tentative ferait pire. On le dit, chiffre à l’appui, et l’administrateur tranche.
| Code d’erreur | Sens |
|---|---|
RESTORE_NO_BACKUP |
aucune sauvegarde des comptes pour ce routeur |
RESTORE_BACKUP_UNREADABLE |
sauvegarde illisible — refus, plutôt que restaurer zéro compte |
RESTORE_BACKUP_EMPTY |
la sauvegarde ne contient aucun compte restaurable |
RESTORE_USERS_FAILED |
restauration interrompue — RADIUS n’a pas été coupé |
RESTORE_INCOMPLETE |
moins de comptes que prévu — RADIUS reste actif |
5. Ce que le gérant reçoit, étape par étape
Notifications de type RADIUS_MIGRATION, une par étape :
| Étape | Message |
|---|---|
SAUVEGARDE |
« Sauvegarde de votre configuration (8 707 comptes) — faite. » |
IMPORT |
« Vos 8 707 comptes ont été copiés dans le serveur cloud. » |
RETRAIT_LOCAL |
« 8 706 comptes locaux retirés — ils vivent maintenant dans le cloud. » |
ARMEMENT |
« En attente de la première connexion d’un de vos clients. » |
ECHEC |
« La bascule s’est arrêtée : … Rien n’a été supprimé sur votre routeur. » |
Puis, type RADIUS_PREMIERE_AUTH, les cinq premières connexions réelles :
« Connexion 1/5 validée ✅ — TM-C4D835 vient de se connecter via le serveur cloud sur J+Pro Ax3. La bascule fonctionne. »
C’est la preuve que le gérant attend. Un « service opérationnel » sans nom d’usager ne prouve rien — c’est exactement ce qu’affichait l’ancien flux pendant que le hotspot continuait de répondre en local. Au-delà de cinq, le système se tait : il a compris qu’il a compris.
La confirmation est automatique depuis le 2026-08-29 (cron RADIUS_MIGRATION_WATCH, toutes
les 3 min). Auparavant elle exigeait un clic d’administrateur, et personne ne le faisait.
6. GET /affluence — ce que le gérant lit sur son réseau
Lecture ouverte au client : c’est son réseau.
{
"disponible": true,
"connexions": 128,
"clients": 34,
"refus": 3,
"pic": { "heure": 19, "connexions": 41 },
"connexions_par_client": 3.8,
"par_heure": [0, 0, "…"],
"boucles": []
}
connexions≠clients. Un même usager se réauthentifie plusieurs fois par session. Confondre les deux gonfle le chiffre d’un facteur 3 à 10 et ferait croire au gérant une affluence qu’il n’a pas. Affichez les deux.pic.heureest dans le fuseau du routeur. 23 h UTC, c’est minuit à Porto-Novo — un pic annoncé à la mauvaise heure ne sert à rien pour décider des horaires d’ouverture.bouclesliste les comptes qui se reconnectent plus de 200 fois par jour. Ce n’est pas de l’affluence, c’est une panne — signalez-le, ne le comptez pas comme fréquentation.disponible: falseavecmotif: "SOURCE_INDISPONIBLE": n’affichez pas un relevé à zéro. « 0 connexion » et « je ne sais pas » ne veulent pas dire la même chose.
Le relevé de fin de journée
Chaque soir à 20 h UTC (21 h au Bénin), type RADIUS_AFFLUENCE :
« 128 connexion(s) réussie(s) aujourd’hui sur J+Pro Ax3, pour 34 client(s). Pic de fréquentation vers 19 h. 3 connexion(s) refusée(s). »
Seuls les clients qui ont eu de l’activité sont notifiés. Envoyer « 0 connexion » chaque soir à un gérant dont le routeur dort est le meilleur moyen de lui faire couper les notifications — et il ne verrait plus les vraies.
7. Réglages
| Variable | Défaut | Effet |
|---|---|---|
RADIUS_MIGRATION_WATCH_ENABLED |
true |
confirmation auto + premières connexions, toutes les 3 min |
RADIUS_READY_AUTO_CONFIRM_ENABLED |
true |
(était false, et aucune cron ne l’appelait) |
RADIUS_AFFLUENCE_REPORT_ENABLED |
true |
relevé de fin de journée à 20 h UTC |