Reporting
Le reporting du Labo du Yeti agrège trois flux distincts : la consommation IA (tokens et coût estimé), les logs d'audit de toutes les actions sensibles, et les snapshots de stats du site exposés via API pour alimenter une plateforme cross-sites externe. Cette page décrit l'état actuel du dispositif, les endpoints disponibles, le modèle de données associé, et les pistes d'intégration externes (Search Console, Analytics) prévues à la roadmap.
/api/stats, /api/version et /api/ai/usage via tokens API long-lived.
Dashboard de consommation IA
L'écran /admin/ia (capability settings_ai, accès owner et admin uniquement) expose un dashboard complet de la consommation IA du CMS. Chaque appel sortant vers un fournisseur (Anthropic, OpenAI) est instrumenté dans backend/src/services/ai-service.js et persisté dans la table ai_usage au moyen du helper aiUsageInsert().
Modèle ai_usage
Chaque ligne de la table représente un appel IA unique. Les colonnes sont prévues pour permettre des agrégats par jour, par type de génération et par fournisseur sans recourir à un OLAP externe.
| Colonne | Type | Description |
|---|---|---|
id | INTEGER | Auto-increment, clé primaire |
timestamp | TEXT (ISO 8601) | Date/heure de l'appel |
kind | TEXT | cocon, page, article, blog, other |
action | TEXT | Libellé fonctionnel (ex. cocon_html_generated, resolve_inline) |
provider | TEXT | anthropic, openai |
model | TEXT | Modèle exact au moment de l'appel (ex. claude-opus, gpt-4o-mini) |
input_tokens | INTEGER | Tokens entrants remontés par l'API (0 si non disponible) |
output_tokens | INTEGER | Tokens sortants remontés par l'API |
success | INTEGER | 1 = OK, 0 = erreur (réseau, quota, validation) |
error_code | TEXT | Status HTTP ou mot-clé si success = 0 |
duration_ms | INTEGER | Latence totale en millisecondes |
resource_id | TEXT | ID de la page/article/cocon associé (corrélation) |
Trois indices accélèrent les requêtes : ai_usage_ts (DESC sur timestamp), ai_usage_kind et ai_usage_success. Cf. la page Base de données pour le schéma SQL complet.
Helpers d'agrégation
Le module backend/src/utils/db-store.js expose plusieurs fonctions d'agrégation calculées en SQL natif (pas de chargement complet de la table en mémoire) :
aiUsageSummary({ from, to, kind })— totaux globaux :total_calls,total_failures,total_input_tokens,total_output_tokens.aiUsageByKind({ from, to })— agrégat parkind(cocon/page/article/blog/other), retourne un tableau[{ kind, calls, failures, input_tokens, output_tokens }].aiUsageByDay({ from, to, kind })— agrégat par jour (cléYYYY-MM-DD) pour alimenter le sparkline du dashboard.aiUsageRecent({ from, to, kind, limit, offset })— log paginé des derniers appels pour inspection/debug.
Coût estimé via global.ai.pricing
Le coût n'est pas stocké en base : il est recalculé à chaque rendu du dashboard à partir du tarif courant. Cela permet de mettre à jour les prix sans rejouer l'historique. Le store global contient le bloc ai.pricing :
{
"ai": {
"pricing": {
"openai": { "input_per_1k": 0.00015, "output_per_1k": 0.0006 },
"anthropic": { "input_per_1k": 0.003, "output_per_1k": 0.015 }
}
}
}
La formule appliquée pour chaque ligne d'agrégat est :
cout_usd = (input_tokens / 1000) * pricing[provider].input_per_1k
+ (output_tokens / 1000) * pricing[provider].output_per_1k;
input_per_1k / output_per_1k sont par défaut ceux des modèles d'entrée de gamme. Si la production utilise des modèles premium (Claude Opus, GPT-4 Turbo), un owner doit ajuster manuellement global.ai.pricing via /admin/ia. Le coût affiché reste indicatif et ne remplace pas la facturation officielle du fournisseur.
Filtres & export du dashboard
L'écran propose une interface dense pour explorer les données :
- Filtre dates — sélecteur
from/topropagé aux endpointsGET /api/ai/usageet au CSV. - Filtre
kind— pillscocon/page/article/blog/other(cumulables). Permet d'isoler par exemple le coût engendré par les seules pages cocon, qui sont massivement parallélisées. - Sparkline quotidien — alimenté par
aiUsageByDay(), repère visuellement les pics de génération. - Tableau récapitulatif par
kind— coût $USD calculé à la volée, taux d'échec, ratio tokens in/out. - Export CSV —
GET /api/ai/usage/export.csvprotégé paraiCallLimiter(10/min) en plus de la capability et du rôle. Le CSV est généré côté serveur sans buffer intermédiaire, compatible avec un import direct dans un tableur.
Endpoints /api/ai/*
| Méthode | Endpoint | Accès | Rôle |
|---|---|---|---|
| GET | /api/ai/status | requireAuth | Tous |
| GET | /api/ai/config | capability settings_ai | owner, admin |
| PUT | /api/ai/config | capability settings_ai | owner, admin |
| GET | /api/ai/usage | capability settings_ai | owner, admin |
| GET | /api/ai/usage/export.csv | capability settings_ai + aiCallLimiter | owner, admin |
Voir aussi Moteur IA pour le détail des appels instrumentés et Authentification & RBAC pour la matrice des capabilities.
Logs d'audit
Toutes les actions sensibles du CMS sont tracées dans un journal append-only. Le helper logger.js (alias logsInsert()) écrit dans la table SQLite logs, optimisée par quatre indices (timestamp DESC, type, action, user_id) pour rester performante sur de très gros volumes sans purge.
Modèle logs
| Colonne | Type | Description |
|---|---|---|
id | INTEGER | Auto-increment |
timestamp | TEXT | ISO 8601 |
type | TEXT | Catégorie (ex. page_created, user_login, extension_installed) |
action | TEXT | Libellé fonctionnel libre |
resource_id | TEXT | ID de la ressource impactée (page, user, extension, token…) |
user_id | TEXT | Auteur de l'action (référence users.id, sans FK pour append-only) |
message | TEXT | Description libre |
meta | TEXT | JSON sérialisé pour contexte structuré |
logs.user_id pour garantir la performance d'insertion et la conservation des entrées même après suppression d'un compte utilisateur. La table n'est jamais purgée par le code ; un owner peut la tronquer manuellement via un export + restore si nécessaire.
UI /admin/logs
L'écran /admin/logs (capability logs, accès owner, admin, webmaster) consomme GET /api/logs avec pagination indexée DB. Il propose :
- Filtre par type — pills correspondant aux catégories existantes (login, page, cocon, extension, backup, capabilities, etc.).
- Recherche full-text — sur
action,resource_id,messageetmetasimultanément (vialogsList({ q })). - Plage de dates — paramètre
since. - Pagination —
limit+offset, comptage total vialogsCount(). - Export CSV — colonnes prêtes pour Excel/Sheets (timestamp ISO, séparateur virgule, échappement
"). - Export TXT — format mono-ligne lisible
[YYYY-MM-DDTHH:mm:ssZ] type/action user=… resource=… messagepour grep / archivage.
Événements tracés (non exhaustif)
- Auth :
user_login,user_login_failed,user_2fa_verified,password_reset_* - Pages / Articles / Cocon :
page_created,page_updated,page_deleted,cocon_generated - Capabilities & RBAC :
capabilities_matrix_updated,capabilities_reset,user_role_changed - Extensions :
extension_installed,extension_activated,extension_deactivated,extension_uninstalled - API tokens :
api_token_created,api_token_revoked - Backup :
backup_export,backup_restore - Build :
build_started,build_finished,sitemap_generated
API tokens long-lived
Les tokens API sont destinés aux intégrations machine-to-machine : reporting cross-sites, serveurs MCP, scripts CI/CD, agrégateurs externes. Ils contournent JWT/2FA mais s'authentifient via un Authorization: Bearer cmstok_… et héritent du rôle de l'utilisateur créateur.
Modèle api_tokens
| Colonne | Type | Description |
|---|---|---|
id | TEXT (UUID) | Clé primaire |
user_id | TEXT | FK users.id ON DELETE CASCADE (le token meurt avec son créateur) |
name | TEXT | Libellé utilisateur (ex. Reporting agence, MCP desktop) |
token_hash | TEXT UNIQUE | Hash SHA-256 du token brut |
scopes | TEXT | read par défaut |
created_at | TEXT | ISO 8601 |
expires_at | TEXT (nullable) | Expiration optionnelle |
last_used_at | TEXT (nullable) | Mis à jour à chaque requête authentifiée (helper apiTokensTouchLastUsed()) |
revoked_at | TEXT (nullable) | Soft delete |
cmstok_ suivi d'un secret aléatoire de 32 octets encodés. Seul le hash SHA-256 est stocké en base — si l'utilisateur perd le token brut, il doit en régénérer un nouveau.
Héritage de rôle et scope
À chaque requête authentifiée par token, le middleware requireAuth :
- Détecte le préfixe
cmstok_dans l'en-têteAuthorization. - Hash SHA-256 du token reçu, lookup via
apiTokensFindByHash(). - Vérifie
expires_atetrevoked_at. - Charge le user via
usersFindById(token.user_id). - Injecte
req.user(avec le rôle du créateur) puisapiTokensTouchLastUsed(token.id)en arrière-plan.
Conséquence : un token créé par un editor n'aura jamais accès à /api/ai/usage (capability settings_ai non possédée). Pour un reporting agence, on crée le token sous un compte admin ou owner dédié à la machine.
Endpoints de gestion
| Méthode | Endpoint | Accès |
|---|---|---|
| GET | /api/auth/tokens | capability settings_tokens |
| POST | /api/auth/tokens | capability settings_tokens |
| DELETE | /api/auth/tokens/:id | capability settings_tokens |
La capability settings_tokens est par défaut accordée à owner et admin seulement (cf. Authentification & RBAC).
Snapshot /api/stats
L'endpoint GET /api/stats retourne un snapshot synthétique du site, conçu pour être consommé par une plateforme de reporting cross-sites en mode pull. Il est protégé par requireAuth et accepte aussi bien un JWT qu'un token API.
Payload
{
"site": {
"name": "Le Labo du Yeti",
"url": "https://example.com",
"default_locale": "fr"
},
"counts": {
"pages": 42,
"articles": 17,
"cocon_groups": 5,
"cocon_pages": 240,
"media": 312,
"users": 4
},
"version": "0.1.0",
"generated_at": "2026-06-01T08:30:00Z"
}
Ce snapshot est calculé en SQL direct (SELECT COUNT(*)) et peut être interrogé à haute fréquence sans coût. Aucune information sensible (emails, hashes, clés IA) n'est exposée.
Endpoint /api/version
GET /api/version retourne la version du CMS (lue depuis package.json) ainsi qu'un snapshot statistiques. Utile pour qu'une plateforme cross-sites détecte d'un coup d'œil les instances en retard de mise à jour.
{ "version": "0.1.0", "snapshot": { "pages": 42, "articles": 17 } }
Endpoints publics (sans auth)
| Méthode | Endpoint | Usage |
|---|---|---|
| GET | /api/health | Ping uptime (monitoring externe type UptimeRobot) |
| GET | /api/branding | Données branding filtrées pour l'écran Login |
Architecture reporting cross-sites
Le modèle SaaS du Labo du Yeti est hybride : 1 client = 1 instance Plesk + DB + domaine. Pour offrir une vue agence centralisée, on déploie une plateforme externe qui interroge périodiquement chaque instance.
Schéma pull (cron côté plateforme)
- Sur chaque instance CMS client, un
ownercrée un token API dédié (nom :Reporting agence), expiration 12 mois. - La plateforme stocke
{ instance_url, token }par client. - Un cron (5 min, 1 h, ou 1 j selon la criticité) appelle en parallèle
/api/health,/api/version,/api/statset/api/ai/usagesur toutes les instances. - Les payloads sont stockés dans une base centrale (PostgreSQL, ClickHouse, BigQuery selon le volume).
- Un dashboard agence consomme cette base avec graphiques croisés (coût IA agrégé, sites en panne, instances obsolètes).
Sécurité du dispositif
- Token long-lived par instance — pas de credentials partagés. Si une instance est compromise, on révoque seulement son token.
- Capability
settings_airequise pour/api/ai/usage— créer le token sous un compteadmindédié. - Helmet + CORS + rate-limiting côté CMS (cf. Sécurité).
- HTTPS obligatoire sur toutes les instances (géré par Plesk, cf. Déploiement).
Intégrations futures (roadmap)
Trois axes sont prévus à la roadmap pour enrichir le reporting au-delà des données internes au CMS.
Google Search Console
Branchement d'API searchconsole.googleapis.com avec compte de service Google Cloud. Stockage du service_account.json dans global.integrations.search_console (chiffré, non exposé aux extensions). Données ciblées :
- Impressions / clics / CTR / position moyenne par URL.
- Top requêtes par page (pour orientation éditoriale du blog et du cocon).
- Erreurs d'indexation et pages exclues.
- Sitemap status (validation de
sitemap.xmlgénéré parbuildSite()).
Le dashboard /admin/seo affichera ces données par page CMS via corrélation page.slug ↔ URL Search Console.
Google Analytics 4
Branchement de la Data API v1 avec le même compte de service. Données ciblées :
- Sessions / utilisateurs / pages vues par URL.
- Durée moyenne par page (signal qualité éditoriale).
- Sources de trafic (organique / direct / référent).
- Conversions formulaires (corrélation avec table
form_submissionsdu module Forms).
Plateforme reporting agence
Application séparée (hors monorepo) qui implémente le schéma pull décrit plus haut. Au programme :
- Vue liste de tous les sites clients (status, version, dernier build).
- Coût IA total agrégé sur la flotte avec ventilation par client.
- Alertes mail/Slack si une instance est down ou si le coût IA dépasse un seuil.
- Mode portail client où chaque client voit uniquement ses propres données (séparation logique par
instance_id). - Export PDF mensuel automatique par client (synthèse pages publiées, coût IA, trafic Search Console).
Bonnes pratiques
- Ne jamais purger
ai_usagenilogs— la table est conçue pour rester append-only. Un export + restore via API backup est la seule procédure légitime de réduction de volume. - Vérifier les tarifs
global.ai.pricingavant toute restitution client : le coût affiché est strictement indicatif. - Créer un token API par intégration (et non un token mutualisé). Faciliter la révocation ciblée en cas de fuite.
- Surveiller
last_used_atdes tokens — un token jamais utilisé depuis sa création peut être révoqué sans risque. - Préférer
/api/statsà/api/ai/usagepour les snapshots haute fréquence — le second nécessite la capabilitysettings_aiet déclenche un agrégat SQL plus lourd.
Références code
backend/src/utils/db-store.js— helpersaiUsageInsert,aiUsageSummary,logsList,apiTokens*.backend/src/services/ai-service.js— instrumentation des appels IA.backend/src/routes/ai.js— endpoints/api/ai/usageet export CSV.backend/src/routes/logs.js— endpoint/api/logspaginé.backend/src/routes/auth.js— gestion des tokens API (/api/auth/tokens).backend/src/middleware/require-auth.js— détection JWT vscmstok_.frontend/src/pages/AdminIa.jsx— UI dashboard IA.frontend/src/pages/AdminLogs.jsx— UI logs.