« Connectée ? » — santé des sources, réseaux et boutiques
GET /admin/api/sources?detail=1,GET /admin/api/networks?detail=1etGET /admin/api/stores?detail=1rendent, pour chaque élément, un verdict calculé par le serveur (lib/connexions-verdict.js), ses usages et une bande « Santé ». Sansdetail, chaque route rend le tableau d'avant, à l'identique (de nombreux écrans le lisent) — à un détail près depuis le 2026-10-05 : les secrets y sont masqués"••••••"(colonnesapi_key,api_secret,webhook_secret… et clés demacrosqui nomment un secret ;GET /admin/api/networksrendait la clé en clair). UnPUTdes sources et réseaux qui renvoie ce masque, une valeur vide ou rien garde la clé stockée ; seule une nouvelle valeur la remplace (lib/connexions-secrets.js). L'agent MCP reçoit le même verdict, par la même fonction : ressourcestrackily://sourcesettrackily://networks, outillist_stores.
Le principe : ne dire que ce que le serveur sait
| Il sait | Il ne sait pas (→ null, jamais deviné) |
|---|---|
| Les identifiants réglés, et s'ils se déchiffrent encore | La date d'expiration d'un jeton (aucune colonne) — un jeton refusé se lit sur l'erreur stockée |
Le dernier « Tester la connexion » (depuis la v139) : date, réussite, erreur expurgée — colonnes last_test_at, last_test_ok, last_test_error des trois tables → dernier_test |
L'erreur d'une synchro réseau (synchro.erreur: null) |
| Source Meta : date + erreur de la dernière synchro Insights | L'échec d'une synchro boutique (jamais écrit) |
Réseau : date de la dernière stat réseau enregistrée (network_offer_stats.synced_at) |
Les postbacks du pixel, de /api/lead, du commerce natif ; le code HTTP d'un postback envoyé |
| Boutique : date + statut de la dernière synchro réussie | |
Postbacks journalisés (system_logs) |
Donc : une source Kadam, TikTok, MGID… avec ses identifiants est « API configurée, non vérifiée » (info) tant qu'elle n'a jamais été testée ; un test réussi de moins de 7 jours la fait « Connectée » (ok), un test raté « Test en échec » (bad), un test réussi plus ancien « Test ancien » (info).
Ce qui s'écrit (lib/connexions-test.js) : seul un appel réel à la plateforme — POST /admin/api/sources/:id/test-connection, POST /admin/api/networks/:id/test-connection, POST /admin/api/stores/:id/test, outils MCP test_source_connection et test_store_connection. Un préalable manquant (« Access Token is required »), Google Ads (OAuth non pris en charge) ou une boutique « webhook » ne s'écrivent pas : rien n'a été vérifié. L'erreur est expurgée (jetons d'URL, Bearer, longues chaînes) avant d'être stockée. Une base restée en v138 répond comme avant (le test ne s'écrit pas, dernier_test: null).
Test et synchro : le plus récent l'emporte. Un test raté après la dernière synchro réussie (Meta, stat réseau enregistrée, synchro boutique) → test_en_echec ; une synchro réussie après lui le périme. Un test réussi après une synchro refusée la périme aussi (jeton ressaisi, puis vérifié). Exception : une synchro Meta automatique qui ne passe plus reste synchro_ancienne (le test ne relance pas le cron), et un réseau dont les offres ont du trafic sans aucune stat enregistrée reste jamais_synchronisee même clé acceptée (la synchro des 30 min échoue).
Forme du verdict
{ "code": "jeton_refuse", "st": "bad", "libelle": "Jeton refusé",
"motif": "Meta Ads a refusé le jeton à la dernière synchro (il y a 3 h) : « … ». Il a expiré ou a été révoqué : génères-en un nouveau et ressaisis-le.",
"a_faire": { "geste": "ressaisir_cle", "libelle": "Ressaisir la clé" },
"depuis": "2026-10-05T09:00:00.000Z",
"avertissements": [] }
st : ok (connectée, preuve récente) · warn (à surveiller) · bad (en panne) · idle (au repos) · info (rien de cassé, rien de prouvé). Suivi par postback seulement n'est jamais une panne. Gestes (a_faire.geste) : activer, ressaisir_cle, configurer_api, completer_identifiants, tester, synchroniser, importer_offres, ou null.
L'ordre est la règle
La première ligne qui s'applique l'emporte.
Sources — plateforme détectée par le slug/nom (Kadam, Meta/Facebook, PropellerAds, Taboola, TikTok, MGID, ExoClick, Google) :
| # | code | st | quand |
|---|---|---|---|
| 1 | inactive |
idle | désactivée |
| 2 | jeton_illisible |
bad | plateforme à API et clé chiffrée avec une autre clé serveur |
| 3 | postback_seulement |
idle | aucun client API (Google, plateforme inconnue) |
| 4 | api_non_configuree |
info | aucun identifiant requis réglé |
| 5 | identifiants_incomplets |
warn | une partie seulement (le motif nomme ce qui manque, ex. macros.ad_account_id) |
| 6 | jeton_refuse |
bad | Meta : l'erreur stockée vise le jeton (190, « access token », « expired »…), et aucun test réussi ne l'a suivie |
| 7 | synchro_en_echec |
bad | Meta : autre erreur stockée, aucun test réussi après |
| 8 | test_en_echec |
bad | le dernier test a échoué (l'erreur dans le motif, geste tester, depuis = date du test), aucune synchro réussie plus récente |
| 9 | jamais_synchronisee |
info | Meta : identifiants complets, aucune synchro, pas de test réussi de moins de 7 j |
| 10 | synchro_ancienne |
warn / info | Meta : > 48 h avec synchro auto (warn, même testée), > 7 j sans (info, sauf test réussi récent) |
| 11 | connectee |
ok | Meta : dernière synchro réussie récente ; toute plateforme : dernier test réussi il y a ≤ 7 j (le motif cite la preuve la plus récente) |
| 12 | test_ancien |
info | dernier test réussi il y a plus de 7 j (geste tester) |
| 13 | api_non_verifiee |
info | identifiants complets, jamais testée (ni synchronisée) |
Avertissements : sans_postback (trafic sur 7 j, pas d'URL de postback, plateforme qui en dépend), postbacks_en_echec (échecs journalisés sur 24 h).
Réseaux (client Everflow) : inactive (idle) → jeton_illisible (bad) → api_non_configuree (info, suivi par postback) → test_en_echec (bad : dernier test raté, aucune stat enregistrée depuis) → url_api_absente (warn : la synchro des 30 min l'ignore) → sans_offre (info, sauf test réussi ≤ 7 j) → jamais_synchronisee (warn si ses offres ont du trafic sur 7 j — même clé acceptée —, sinon info ; sans trafic, un test réussi tranche) → synchro_ancienne (> 2 h ; warn / info, même règle) → connectee (ok : stat récente, ou test réussi ≤ 7 j) → test_ancien (info : test réussi > 7 j, sans stat ni trafic).
Boutiques (Shopify, WooCommerce, Stripe ; autre = webhook) : inactive (idle) → jeton_illisible (bad) → webhook_seulement (idle) → api_non_configuree (info) → identifiants_incomplets (warn) → test_en_echec (bad : dernier test raté, aucune synchro réussie depuis) → synchro_partielle (warn) → jamais_synchronisee (info : ni synchro ni test réussi) → synchro_ancienne (> 7 j, info, sauf test réussi ≤ 7 j) → connectee (ok : synchro ≤ 7 j ou test réussi ≤ 7 j) → test_ancien (info : test réussi > 7 j, sans synchro).
Les réponses
GET /admin/api/sources?detail=1
{ "sources": [ {
"id": 3, "name": "Meta principal", "slug": "meta", "is_active": true, "created_at": "…", "notes": "",
"api_url": "", "click_id_param": "click_id", "cost_param": "", "ads_auto_sync": true,
"api_key": "••••••", "macros": { "ad_account_id": "act_123", "client_secret": "••••••" },
"plateforme": { "code": "meta", "libelle": "Meta Ads", "api": true, "synchro": true },
"identifiants": { "etat": "complet", "manquants": [], "jeton": "present" },
"synchro": { "derniere_at": "…", "erreur": null, "auto": true },
"dernier_test": { "at": "2026-10-06T08:12:00.000Z", "ok": false, "erreur": "Meta API: Error validating access token: …" },
"postback": { "configure": false, "hote": null },
"usages": { "campagnes": { "total": 2, "actives": 1, "avec_clics_7j": 1 },
"clics_24h": 120, "clics_7j": 900, "conversions_24h": 4, "conversions_7j": 31,
"postbacks_envoyes_24h": 0, "postbacks_echecs_24h": 0, "dernier_postback_envoye_at": null },
"verdict": { … } } ],
"sante": { "total": 5, "actives": 4, "inactives": 1, "connectees": 1, "a_corriger": 1, "a_surveiller": 0, "sans_api": 2,
"par_st": { "ok": 1, "warn": 0, "bad": 1, "idle": 2, "info": 1 },
"activite_24h": { "clics": 300, "conversions": 9, "postbacks_envoyes": 7, "postbacks_echecs": 0 },
"alerte": { "code": "source_en_panne_campagnes_actives", "libelle": "…", "elements": [ { "id": 3, "name": "…", "code": "jeton_refuse", "motif": "…", "campagnes_actives": 1 } ] } },
"genere_a": "…" }
dernier_test : { at, ok, erreur } (erreur expurgée, null quand le test a réussi) ou null si l'élément n'a jamais été testé — même forme sur les trois listes.
GET /admin/api/networks?detail=1 rend { reseaux, sante, genere_a } ; chaque réseau porte plateforme (everflow), identifiants, synchro { derniere_at, erreur: null, auto }, dernier_test, postback { configure } et usages { offres { total, actives }, campagnes, clics_24h, clics_7j, conversions_24h, conversions_7j, postbacks_recus_24h, dernier_postback_recu_at }. GET /admin/api/stores?detail=1 rend { boutiques, sante, genere_a } ; synchro { derniere_at, statut, erreur: null, auto: false }, usages { produits, campagnes, clics_24h, clics_7j, conversions_24h, conversions_7j, derniere_conversion_at }.
Alerte (une seule, la plus grave) : un élément bad qu'utilisent des campagnes actives (source_en_panne_campagnes_actives, reseau_…, boutique_…) ; sinon, sources : postbacks_en_echec ; réseaux : stats_reseau_bloquees (warn + trafic). Le suivi par postback n'alerte jamais.
Ce qui est compté
- Campagnes : non archivées qui la citent (source :
source_id; réseau : offre directeoffer_id; boutique :store_id), dont actives ; plus les campagnes distinctes qui lui ont amené des clics sur 7 j. - Clics 24 h / 7 j, bots compris (comme la liste d'avant). Conversions 24 h / 7 j, rejetées exclues.
- Postbacks : ceux que le journal connaît —
sent_postback(chemins/postbacket import CSV ; un échec = réseau ou adresse refusée, pas un HTTP 4xx/5xx) etreceived_postback(quand le clic est retrouvé). Vider la page Logs fait baisser ces comptes. - Une seule requête SQL par liste (CTE agrégées), quel que soit le nombre d'éléments.
Secrets
Aucune réponse du détail ne porte de secret : colonnes chiffrées → "••••••" (réglée) ou "" ; clés de macros qui nomment un secret → "••••••" ; erreurs stockées expurgées (jetons d'URL, Bearer, chiffrés, longues chaînes) ; URL de postback réduite à son hôte ; settings des boutiques non rendu. Renvoyer "••••••" dans un formulaire ne touche pas la clé (encryptRowSecrets l'ignore).