Trackily Docs

« Connectée ? » — santé des sources, réseaux et boutiques

GET /admin/api/sources?detail=1, GET /admin/api/networks?detail=1 et GET /admin/api/stores?detail=1 rendent, pour chaque élément, un verdict calculé par le serveur (lib/connexions-verdict.js), ses usages et une bande « Santé ». Sans detail, 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 "••••••" (colonnes api_key, api_secret, webhook_secret… et clés de macros qui nomment un secret ; GET /admin/api/networks rendait la clé en clair). Un PUT des 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 : ressources trackily://sources et trackily://networks, outil list_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 directe offer_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 /postback et import CSV ; un échec = réseau ou adresse refusée, pas un HTTP 4xx/5xx) et received_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).