Documentation J+SERVICES Guides Référence API

Pro Hub — note d’intégration web & plan de tests

2026-08-21. Le backend du Pro Hub est déployé. Ce document s’adresse au développeur de l’application web app.mikhmoai.com, qui porte désormais toute la partie technicien.

Répartition des rôles. Le Pro Hub — annuaire, progression, quota, supervision, portefeuille — se développe côté plateforme web, pas dans l’application mobile. Celle-ci reste concentrée sur le parcours client, pour ne pas l’alourdir. Les deux consomment la même API sur live.jmoai.net ; seules les surfaces diffèrent.

Le contrat de référence reste Contrat API — Pro Hub. Ici on teste.


1. À lire avant de coder — six points qui vont te surprendre

1. Les canaux temps réel ont changé de nom. support_presencesupport:presence et support_missionssupport:missions. Les deux-points ne sont pas cosmétiques : un canal sans eux tombe dans le namespace par défaut de Centrifugo, sans présence, sans historique, sans join/leave. Abonne-toi aux noms avec deux-points, sinon tout marchera « à moitié » sans erreur visible.

2. price: null veut dire « sur devis », pas « gratuit ». Plusieurs techniciens ont répondu « Variable » à la question du tarif. On n’invente pas un nombre à leur place. Affiche une mention, jamais 0.

3. status_detail vaut OFFLINE dès que isOnline est faux. Un technicien hors ligne n’est ni disponible ni occupé : il est absent. N’affiche pas de pastille verte sur la base du seul status_detail stocké.

4. La réponse est enveloppée. Toutes les routes rendent { "success": true, "data": …, "result": …, "domain": "backend" }. Lis data ; result en est le miroir. Un success: false est une vraie erreur, pas un résultat vide.

5. La liste ne contient que des experts actifs ET certifiés. Si un technicien que tu connais n’apparaît pas, ce n’est pas un bug de pagination : il n’est pas certifié.

6. Les tags viennent d’une reprise volontairement prudente. Un tag n’a été posé que si le mot figurait littéralement dans l’expertise saisie. Les profils existants sont donc pauvres en tags tant qu’ils n’auront pas été complétés à la main. Un filtre qui ne rend rien peut être un profil incomplet, pas une régression.


2. Points d’entrée

Base : https://live.jmoai.net/api/v1

# Méthode Chemin Auth
1 GET /support/professionals non
2 GET /support/professionals/:id non
3 POST /support/presence oui
4 GET /support/centrifugo-token oui
5 POST /support/attachments oui
6 POST /support/wallet/tickets oui
7 POST /support/tickets/:id/claim oui
8 POST /support/tickets/:id/notes oui
9 GET /support/tickets/:id/notes oui
10 POST /support/tickets/:id/review oui

3. Série de tests

A — Annuaire

Test Requête Attendu Ce que ça prouve
A1 GET /support/professionals 200, data[], meta{total,page,limit} La forme du contrat est respectée
A2 ?tags=MikroTik Seuls les profils portant ce tag Le filtre par tag opère sur un vrai tableau
A3 ?tags=MikroTik,Fibre Profils ayant au moins un des deux La correspondance est un OU, pas un ET
A4 ?country=BJ Filtré ; ?country=bj donne le même résultat Code ISO, insensible à la casse
A5 ?status=nimportequoi 400 INVALID_STATUS Les valeurs libres sont rejetées, pas ignorées
A6 ?limit=1&page=2 2ᵉ profil, meta.total inchangé La pagination est cohérente
A7 ?status=online meta.total == nombre de lignes rendues Le total ne ment pas quand on filtre sur la présence
A8 GET /support/professionals/<uuid-inexistant> 404 PROFESSIONAL_NOT_FOUND Pas de 200 vide trompeur
A9 GET /support/professionals/:id sur un profil réel bio, tier_level, votes, reviews[] en plus La fiche détaillée est bien enrichie

B — Présence

Test Requête Attendu
B1 POST /support/presence {"online":true} 200, data.isOnline = true
B2 Aussitôt après : GET /support/professionals?status=online Le technicien y figure
B3 Attendre 95 s sans battement, refaire B2 Il n’y figure plus
B4 POST /support/presence {"online":false} Disparaît immédiatement
B5 Sans jeton 401

B3 est le test important : le battement expire tout seul au bout de 90 s. Envoie-en un toutes les 60 s tant que l’écran technicien est au premier plan. Un technicien qui tue l’app doit disparaître de la liste sans rien faire de plus.

C — Temps réel

Test Action Attendu
C1 GET /support/centrifugo-token 200 + token (JWT). Un 503 REALTIME_UNCONFIGURED signalerait un backend mal configuré — remonte-le
C2 Connexion WebSocket avec ce jeton Connexion acceptée
C3 Jeton bricolé à la main Connexion refusée
C4 S’abonner à support:presence, puis faire B1 depuis un autre appareil Réception d’un PRO_STATUS_CHANGED
C5 S’abonner à support:missions, puis créer un ticket rémunéré Réception d’un NEW_BOUNTY_TICKET
C6 Deux techniciens abonnés, l’un prend le ticket L’autre reçoit TICKET_CLAIMED

Charges utiles exactes : voir le contrat. Elles sont vérifiées côté serveur et conservées dans l’historique du canal (24 h), donc reproductibles.

D — Missions rémunérées et séquestre

Test Requête Attendu
D1 POST /support/wallet/tickets avec un solde insuffisant 400 SOLDE_INSUFFISANT + balance, required, missing
D2 Idem avec un solde suffisant 201, ticket en OPEN, somme bloquée
D3 Vérifier le portefeuille après D2 Le solde a bien diminué
D4 POST /support/tickets/:id/claim 200, ticket assigné
D5 Refaire D4 depuis un second compte 409 — le premier a verrouillé
D6 Après D4, recharger l’annuaire Le technicien passe BUSY si le ticket est urgent
D7 Clôturer le ticket Retour à AVAILABLE, ticketsResolved +1
D8 POST /support/tickets/:id/review {rating:4} 200, la note moyenne du technicien bouge
D9 review avec rating: 9 400

D5 est le test de la course. Lance les deux requêtes au même instant si tu peux : le verrou est en base, pas dans le code, donc il tient même en simultané.

D6 : le passage en BUSY ne concerne que les tickets urgents. Un technicien sur une demande de fond reste joignable pour une urgence — c’est voulu.

E — Notes internes

Test Requête Attendu
E1 POST /support/tickets/:id/notes en tant que technicien assigné 201
E2 Idem depuis le compte client du ticket 403 NOT_ASSIGNED
E3 GET .../notes depuis le compte client 403
E4 POST avec content vide 400 EMPTY_NOTE
E5 Sur un ticket inexistant 404 TICKET_NOT_FOUND

E2 et E3 sont les tests qui comptent : une note interne qui fuit vers le client n’a plus aucune raison d’exister.

F — Pièces jointes

Test Action Attendu
F1 POST /support/attachments {fileName, mimeType} 200 + uploadUrl, objectKey, fileUrl
F2 PUT du fichier sur uploadUrl 200, sans en-tête d’authentification
F3 Ouvrir fileUrl 200, le fichier s’affiche
F4 Envoyer un message avec attachment_url = objectKey 201, l’écho contient un attachment_url ouvrable
F5 GET /channels/:id/messages Chaque attachment_url est ouvrable
F6 Réutiliser un uploadUrl après 15 min Refusé — expiré
F7 Modifier un caractère de l’URL Refusé — signature invalide
F8 Ouvrir l’objet sans la partie signée (?X-Amz-…) 403 — le bucket n’est pas public

Ne mets pas attachment_url en cache longtemps. C’est un lien signé qui expire au bout de 15 minutes, resigné à chaque lecture de la conversation. Il n’est pas stable, et c’est volontaire : le bucket reste privé, aucun lien ne survit à sa fuite. Pour réafficher une pièce jointe plus tard, recharge les messages.

Ce que tu renvoies dans attachment_url à l’envoi doit être l’objectKey, pas le lien signé.

4. Spécificités du client WEB — à lire en premier

Un navigateur est soumis à deux contrôles qu’une application native ne subit pas. C’est là que se perdent les premières heures.

1. L’origine est vérifiée au WebSocket. Centrifugo compare l’en-tête Origin à une liste blanche. https://app.mikhmoai.com y est déclaré et vérifié — une origine absente reçoit un 403 dès la poignée de main, avant tout message, et le symptôme (« connexion impossible ») ne dit rien de la cause.

⚠️ Conséquence directe pour ton poste de développement : un serveur local (http://localhost:5173, 127.0.0.1:3000…) sera refusé. Demande-nous d’ajouter ton origine de développement — c’est une ligne de configuration côté serveur, tu ne peux pas la contourner depuis le navigateur.

Mesuré :

Origine Résultat
https://app.mikhmoai.com ✅ accepté
https://app.jmoai.net ✅ accepté
une origine non déclarée 403 au handshake
aucune (app native) ✅ accepté — non concernée

2. Le CORS de l’API. https://app.mikhmoai.com figure déjà dans les origines autorisées du backend. Rien à faire, mais si tu sers depuis un autre nom, dis-le-nous.

3. L’authentification accepte deux formes. Un jeton Authorization: Bearer … ou la session par cookie. Le web peut utiliser l’une ou l’autre — mais si tu prends le cookie, pense à credentials: 'include' sur tes fetch, sinon le navigateur ne l’enverra pas et tu recevras un 401 TOKEN_MISSING en te croyant connecté.


5. Adresses et état du déploiement

Usage URL
API REST https://live.jmoai.net/api/v1
WebSocket temps réel wss://chat.jmoai.net/connection/websocket
Pièces jointes https://chat.jmoai.net/expert-hub-media/… (URL rendue par l’API)

Le TLS est en place depuis le 2026-08-21 : domaine dédié chat.jmoai.net, certificat Let’s Encrypt à renouvellement automatique. Vérifié de bout en bout — envoi pré-signé accepté, relecture conforme, et accès sans signature refusé en 403.

Ce domaine est volontairement séparé de vpn.mikhmoai.com, qui porte WebFig et le VPN. Sa racine répond 404 : ni la console MinIO ni l’API d’administration de Centrifugo n’y sont exposées.

⚠️ Mets l’URL en configuration, pas en dur. Elle a déjà changé une fois aujourd’hui, et un jour on voudra un domaine par environnement.

Les profils sont pauvres. Deux techniciens en base, un seul certifié, zéro mission terminée, aucun tarif chiffré. Prévois l’affichage des cas vides : pas d’avis, pas de tarif, pas de tag.

6. Contrat d’erreur — formes réelles, relevées en production

Toute erreur REST sort sous cette forme. error.code est le seul champ à tester ; le message est susceptible de changer.

{ "success": false, "error": { "code": "…", "message": "…" }, "domain": "backend" }

Réponses réellement capturées :

Situation HTTP error.code
?status= invalide 400 INVALID_STATUS
Profil inconnu 404 PROFESSIONAL_NOT_FOUND
Aucun jeton 401 TOKEN_MISSING
Jeton illisible / mauvaise signature 401 TOKEN_INVALID
Jeton expiré 401 TOKEN_EXPIRED
Note sur un ticket non assigné 403 NOT_ASSIGNED
Note vide 400 EMPTY_NOTE
Quota de récupération dépassé 429 RECOVERY_RATE_LIMIT
Temps réel non configuré 503 REALTIME_UNCONFIGURED
Solde insuffisant 400 SOLDE_INSUFFISANT (+ balance, required, missing)
Ticket déjà pris 409 RACE_CONDITION

TOKEN_EXPIRED et TOKEN_INVALID sont distincts depuis le 2026-08-21. Avant, les deux sortaient en SERVER_ERROR et l’app ne pouvait pas les séparer d’une panne. Le premier se résout en rafraîchissant la session ; le second exige une reconnexion complète. Un champ message est conservé à la racine pour compatibilité — ne t’appuie pas dessus.

7. Centrifugo — comportements mesurés

Testés en connexion réelle sur wss://chat.jmoai.net/connection/websocket :

Cas Ce que fait Centrifugo
Jeton valide Connexion acceptée, la réponse connect porte ttl: 3599
Jeton expiré Erreur applicative code: 109 « token expired » — la connexion n’est PAS fermée
Signature invalide Fermeture, code de déconnexion 3500
Aucun jeton Fermeture, code de déconnexion 3501

Ce qu’il faut en faire :

Le jeton vit 1 heure.

8. Si un test échoue

Donne-nous la requête complète, le code HTTP et le corps de la réponse. Les codes d’erreur sont explicites (INVALID_STATUS, NOT_ASSIGNED, SOLDE_INSUFFISANT, RACE_CONDITION, REALTIME_UNCONFIGURED) : c’est le code, pas le message, qui identifie la cause.

Un 503 REALTIME_UNCONFIGURED et un 429 RECOVERY_RATE_LIMIT ne sont pas des bugs de l’app : ce sont des états serveur à nous signaler tels quels.