bricks-builder-mcp 3.8.0 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/package.json +1 -1
- package/server.js +660 -222
package/README.md
CHANGED
|
@@ -108,8 +108,14 @@ Claude Code détectera automatiquement le skill au prochain démarrage.
|
|
|
108
108
|
**Éléments**
|
|
109
109
|
- `find_elements`, `get_element`, `update_element` (avec param `label` pour renommer dans le builder), `add_element`, `batch_add`, `delete_element`, `reorder_sections`
|
|
110
110
|
|
|
111
|
-
**⭐ Vérification visuelle**
|
|
112
|
-
- `verify_element` — screenshot crop + report
|
|
111
|
+
**⭐ Vérification visuelle (v3.10+)**
|
|
112
|
+
- `verify_element` — screenshot crop + report. **À utiliser après chaque batch_add significatif.**
|
|
113
|
+
- Compare computed styles vs settings attendus, fonts, erreurs JS, débordement horizontal
|
|
114
|
+
- **v3.10 — Multi-viewport en 1 call** : `viewports: ["desktop", "mobile_portrait"]` → 1 report + 1 screenshot par viewport
|
|
115
|
+
- **v3.10 — Cohérence siblings** : text-align mixé entre frères, jumps font-size anormaux
|
|
116
|
+
- **v3.10 — Containers vides anormaux** : tout bloc ≥ 50×50px sans contenu (attrape les wrappers écrasés par `align-items: stretch`)
|
|
117
|
+
- **v3.10 — Santé des médias** : `naturalWidth > 0` sur les `<img>` (lazy-load cassé), `readyState ≥ 2` sur les `<video>`, alt présents
|
|
118
|
+
- Chaque check porte une `severity` (`critical` / `warning` / `info`) et un `hint` actionnable. Désactivable par catégorie via `checks: { sibling_coherence: false }`.
|
|
113
119
|
|
|
114
120
|
**⭐ Upload optimisé (v3.8+)**
|
|
115
121
|
- `upload_local_file`, `upload_local_files_batch` — l'IA donne juste le path local, le MCP server lit le fichier, conversion WebP automatique (-80 à -95% de poids), renommage SEO via le `title`
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "bricks-builder-mcp",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.10.0",
|
|
5
5
|
"description": "Serveur MCP pour piloter Bricks Builder (WordPress) depuis Claude — édition de pages, gestion d'éléments, réordonnancement des sections, vérification visuelle (verify_element), upload optimisé WebP. Communauté Discord : https://discord.gg/rX22zHRzH",
|
|
6
6
|
"homepage": "https://discord.gg/rX22zHRzH",
|
|
7
7
|
"main": "server.js",
|
package/server.js
CHANGED
|
@@ -214,6 +214,116 @@ function generateHint(prop, expected, got) {
|
|
|
214
214
|
return null;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
// ===== v3.10 — Moteur de checks pluggable =====
|
|
218
|
+
// Chaque check est une fonction Node-side qui reçoit le `audit` collecté
|
|
219
|
+
// dans la page (siblings, media, emptyContainers) et retourne des entrées
|
|
220
|
+
// {ok, severity, label, hint, bbox} à pousser dans le report.
|
|
221
|
+
// Sévérités : 'critical' (bug bloquant), 'warning' (probablement un bug),
|
|
222
|
+
// 'info' (juste à savoir). 'ok' false = compte comme échec dans le score.
|
|
223
|
+
|
|
224
|
+
function buildSiblingCoherenceChecks(audit) {
|
|
225
|
+
const checks = [];
|
|
226
|
+
const siblings = audit.siblings || [];
|
|
227
|
+
if (siblings.length === 0) return checks;
|
|
228
|
+
|
|
229
|
+
const all = [audit.self, ...siblings];
|
|
230
|
+
|
|
231
|
+
// 1) text-align mixé entre frères directs
|
|
232
|
+
// Si un seul frère a un text-align différent, c'est souvent un bug visuel
|
|
233
|
+
// (ex: H2 left + sous-titre center sans intention design).
|
|
234
|
+
const aligns = [...new Set(all.map(s => s['text-align']).filter(Boolean))];
|
|
235
|
+
if (aligns.length > 1) {
|
|
236
|
+
checks.push({
|
|
237
|
+
ok: false,
|
|
238
|
+
severity: 'warning',
|
|
239
|
+
label: `text-align mixé entre frères directs (${aligns.join(', ')})`,
|
|
240
|
+
hint: "Frères directs avec text-align différents — souvent un bug visuel. Vérifie l'intention design ou aligne-les sur la même valeur.",
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 2) Jumps de font-size > 2.5x entre frères (info seulement, c'est parfois voulu pour H1/sous-titre)
|
|
245
|
+
const withFontSize = all.filter(s => s['font-size-px'] && s['font-size-px'] > 0);
|
|
246
|
+
if (withFontSize.length >= 2) {
|
|
247
|
+
const sizes = withFontSize.map(s => s['font-size-px']);
|
|
248
|
+
const min = Math.min(...sizes);
|
|
249
|
+
const max = Math.max(...sizes);
|
|
250
|
+
if (min > 0 && max / min > 2.5) {
|
|
251
|
+
checks.push({
|
|
252
|
+
ok: true,
|
|
253
|
+
severity: 'info',
|
|
254
|
+
label: `font-size variable entre frères (${Math.round(min)}px → ${Math.round(max)}px)`,
|
|
255
|
+
hint: "Écart > 2.5x entre frères — normal pour H1/sous-titre, à vérifier sinon.",
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return checks;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function buildEmptyContainerChecks(audit) {
|
|
264
|
+
const checks = [];
|
|
265
|
+
const empties = audit.emptyContainers || [];
|
|
266
|
+
if (empties.length === 0) {
|
|
267
|
+
checks.push({ ok: true, label: "Aucun container vide anormal détecté (≥ 50×50px)" });
|
|
268
|
+
return checks;
|
|
269
|
+
}
|
|
270
|
+
const sorted = empties.slice().sort((a, b) => b.area - a.area).slice(0, 5);
|
|
271
|
+
checks.push({
|
|
272
|
+
ok: false,
|
|
273
|
+
severity: 'warning',
|
|
274
|
+
label: `${empties.length} container(s) visible(s) sans contenu (≥ 50×50px)`,
|
|
275
|
+
got: sorted.map(c => `${c.classes[0] || c.id || '?'} ${c.rect.w}×${c.rect.h}`),
|
|
276
|
+
bboxes: sorted.map(c => c.rect),
|
|
277
|
+
hint: "Conteneurs vides détectés — bug fréquent quand align-items: stretch écrase un block-vide à des proportions absurdes (dot 6px qui devient 180px, etc). Vérifie aspect-ratio fixe ou ajoute du contenu.",
|
|
278
|
+
});
|
|
279
|
+
return checks;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function buildMediaHealthChecks(audit) {
|
|
283
|
+
const checks = [];
|
|
284
|
+
const { images = [], videos = [] } = (audit.media || {});
|
|
285
|
+
|
|
286
|
+
if (images.length > 0) {
|
|
287
|
+
const broken = images.filter(i => !i.loaded);
|
|
288
|
+
if (broken.length === 0) {
|
|
289
|
+
checks.push({ ok: true, label: `${images.length} image(s) chargée(s) (naturalWidth > 0)` });
|
|
290
|
+
} else {
|
|
291
|
+
checks.push({
|
|
292
|
+
ok: false,
|
|
293
|
+
severity: 'critical',
|
|
294
|
+
label: `${broken.length}/${images.length} image(s) PAS chargée(s) (naturalWidth = 0)`,
|
|
295
|
+
got: broken.slice(0, 3).map(i => (i.src || '').split('/').pop() || '(no src)'),
|
|
296
|
+
hint: "Image(s) non chargée(s) — vérifie l'URL src, le lazy-loading qui ne se déclenche pas, ou un 404. À éviter en production.",
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const noAlt = images.filter(i => !i.alt || i.alt.trim().length === 0);
|
|
300
|
+
if (noAlt.length > 0) {
|
|
301
|
+
checks.push({
|
|
302
|
+
ok: false,
|
|
303
|
+
severity: 'warning',
|
|
304
|
+
label: `${noAlt.length}/${images.length} image(s) sans attribut alt`,
|
|
305
|
+
hint: "alt manquant — bloque l'accessibilité et le SEO. Renseigne alt à l'upload via upload_local_file({alt: '...'}).",
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (videos.length > 0) {
|
|
311
|
+
const notLoaded = videos.filter(v => !v.loaded);
|
|
312
|
+
if (notLoaded.length === 0) {
|
|
313
|
+
checks.push({ ok: true, label: `${videos.length} vidéo(s) chargée(s) (readyState ≥ 2)` });
|
|
314
|
+
} else {
|
|
315
|
+
checks.push({
|
|
316
|
+
ok: false,
|
|
317
|
+
severity: 'warning',
|
|
318
|
+
label: `${notLoaded.length}/${videos.length} vidéo(s) pas prête(s) (readyState < 2)`,
|
|
319
|
+
hint: "Vidéo(s) non prête(s) — peut être normal si autoplay désactivé sur mobile. Sinon vérifier src/codec.",
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return checks;
|
|
325
|
+
}
|
|
326
|
+
|
|
217
327
|
// Configuration du fichier de log
|
|
218
328
|
const __filename = fileURLToPath(import.meta.url);
|
|
219
329
|
const __dirname = dirname(__filename);
|
|
@@ -999,7 +1109,7 @@ mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
999
1109
|
// ===== v3.6.0 — VERIFY ELEMENT (vérification visuelle + technique) =====
|
|
1000
1110
|
{
|
|
1001
1111
|
name: "verify_element",
|
|
1002
|
-
description: "⭐ APRÈS CHAQUE batch_add OU update_element SIGNIFICATIF, utilise cet outil. Lance un browser headless, navigue sur la page, scroll vers l'élément, prend un screenshot crop et compare les computed styles avec les settings attendus. Retourne {screenshot
|
|
1112
|
+
description: "⭐ APRÈS CHAQUE batch_add OU update_element SIGNIFICATIF, utilise cet outil. Lance un browser headless, navigue sur la page, scroll vers l'élément, prend un screenshot crop et compare les computed styles avec les settings attendus. v3.10 : ajout de checks généralistes (cohérence siblings, containers vides, santé des médias) + multi-viewport en 1 call (param `viewports`). Retourne {screenshot(s) visibles par Claude, report: {score, checks}, computed}. Force l'évolution petit-à-petit-vérifier-petit-à-petit.",
|
|
1003
1113
|
inputSchema: {
|
|
1004
1114
|
type: "object",
|
|
1005
1115
|
properties: {
|
|
@@ -1008,7 +1118,24 @@ mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1008
1118
|
viewport: {
|
|
1009
1119
|
type: "string",
|
|
1010
1120
|
enum: ["desktop", "tablet", "mobile_landscape", "mobile_portrait"],
|
|
1011
|
-
description: "Taille d'écran à tester (défaut: desktop)",
|
|
1121
|
+
description: "Taille d'écran à tester (défaut: desktop). Ignoré si `viewports` est fourni.",
|
|
1122
|
+
},
|
|
1123
|
+
viewports: {
|
|
1124
|
+
type: "array",
|
|
1125
|
+
items: { type: "string", enum: ["desktop", "tablet", "mobile_landscape", "mobile_portrait"] },
|
|
1126
|
+
description: "v3.10 : tester plusieurs viewports en 1 call. Renvoie un report + screenshot par viewport. Ex: ['desktop', 'mobile_portrait'].",
|
|
1127
|
+
},
|
|
1128
|
+
checks: {
|
|
1129
|
+
type: "object",
|
|
1130
|
+
description: "v3.10 : activer/désactiver des catégories de checks (toutes activées par défaut).",
|
|
1131
|
+
properties: {
|
|
1132
|
+
expected_styles: { type: "boolean", description: "Compare les computed styles aux settings attendus (défaut: true)" },
|
|
1133
|
+
sibling_coherence: { type: "boolean", description: "Détecte text-align mixé et jumps font-size entre frères directs (défaut: true)" },
|
|
1134
|
+
empty_containers: { type: "boolean", description: "Flag les blocs visibles ≥ 50×50px sans contenu (défaut: true)" },
|
|
1135
|
+
media_health: { type: "boolean", description: "Vérifie naturalWidth > 0 sur les <img>, readyState ≥ 2 sur les <video>, alt présents (défaut: true)" },
|
|
1136
|
+
overflow: { type: "boolean", description: "Détecte les débordements horizontaux (défaut: true)" },
|
|
1137
|
+
console_errors: { type: "boolean", description: "Remonte les erreurs JS console (défaut: true)" },
|
|
1138
|
+
},
|
|
1012
1139
|
},
|
|
1013
1140
|
},
|
|
1014
1141
|
required: ["pageId", "elementId"],
|
|
@@ -1102,6 +1229,111 @@ mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1102
1229
|
},
|
|
1103
1230
|
},
|
|
1104
1231
|
|
|
1232
|
+
// ===== v3.9.0 — Custom Post Types =====
|
|
1233
|
+
{
|
|
1234
|
+
name: "list_post_types",
|
|
1235
|
+
description: "Liste tous les post types enregistrés sur le site (built-in pages/posts + CPT custom comme chantier, avis_client, produit). Retourne pour chaque : name, label, supports, taxonomies associées, hierarchical, public, showInRest. À appeler en premier pour découvrir ce qui est dispo.",
|
|
1236
|
+
inputSchema: { type: "object", properties: {} },
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
name: "create_post",
|
|
1240
|
+
description: "Crée un post dans n'importe quel post_type (page, post, ou CPT custom). Supporte meta (ACF compatible — auto-route via update_field si ACF est chargé), taxonomies (slugs OU IDs, création à la volée des termes manquants), featuredImageId. Pour seeder un site avec CPT (galerie, blog, témoignages, etc.).",
|
|
1241
|
+
inputSchema: {
|
|
1242
|
+
type: "object",
|
|
1243
|
+
properties: {
|
|
1244
|
+
postType: { type: "string", description: "Slug du post_type (ex: 'chantier', 'avis_client', 'page')" },
|
|
1245
|
+
title: { type: "string", description: "Titre du post" },
|
|
1246
|
+
content: { type: "string", description: "Contenu HTML (optionnel)" },
|
|
1247
|
+
excerpt: { type: "string", description: "Extrait (optionnel)" },
|
|
1248
|
+
slug: { type: "string", description: "Slug URL (auto-généré depuis title si omis)" },
|
|
1249
|
+
status: { type: "string", enum: ["publish", "draft", "private", "pending", "future"], description: "Statut (défaut: publish)" },
|
|
1250
|
+
featuredImageId: { type: "number", description: "ID de l'image WP (depuis upload_media/upload_local_file)" },
|
|
1251
|
+
meta: { type: "object", description: "Champs personnalisés (ACF compatible). Format {field_name: value}. Pour gallery: array d'IDs. Pour repeater: array d'objects. Pour relationship: array d'IDs." },
|
|
1252
|
+
taxonomies: { type: "object", description: "Format {taxonomy_slug: [term_slug_or_id, ...]}. Crée le terme à la volée s'il n'existe pas." },
|
|
1253
|
+
date: { type: "string", description: "Date publication ISO 8601 (défaut: maintenant)" },
|
|
1254
|
+
author: { type: "number", description: "ID de l'auteur" },
|
|
1255
|
+
},
|
|
1256
|
+
required: ["postType", "title"],
|
|
1257
|
+
},
|
|
1258
|
+
},
|
|
1259
|
+
{
|
|
1260
|
+
name: "update_post",
|
|
1261
|
+
description: "Modifie un post existant (CPT ou natif). Pour modifier juste les ACF, passe uniquement 'meta'. Pour retirer la featured image, passe featuredImageId: 0.",
|
|
1262
|
+
inputSchema: {
|
|
1263
|
+
type: "object",
|
|
1264
|
+
properties: {
|
|
1265
|
+
postId: { type: "number", description: "ID du post à modifier" },
|
|
1266
|
+
title: { type: "string" },
|
|
1267
|
+
content: { type: "string" },
|
|
1268
|
+
excerpt: { type: "string" },
|
|
1269
|
+
slug: { type: "string" },
|
|
1270
|
+
status: { type: "string", enum: ["publish", "draft", "private", "pending"] },
|
|
1271
|
+
featuredImageId: { type: "number", description: "0 pour retirer" },
|
|
1272
|
+
meta: { type: "object" },
|
|
1273
|
+
taxonomies: { type: "object" },
|
|
1274
|
+
date: { type: "string" },
|
|
1275
|
+
},
|
|
1276
|
+
required: ["postId"],
|
|
1277
|
+
},
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
name: "delete_post",
|
|
1281
|
+
description: "Supprime un post. Par défaut → corbeille. force: true → suppression définitive.",
|
|
1282
|
+
inputSchema: {
|
|
1283
|
+
type: "object",
|
|
1284
|
+
properties: {
|
|
1285
|
+
postId: { type: "number" },
|
|
1286
|
+
force: { type: "boolean", description: "Défaut false (corbeille)" },
|
|
1287
|
+
},
|
|
1288
|
+
required: ["postId"],
|
|
1289
|
+
},
|
|
1290
|
+
},
|
|
1291
|
+
{
|
|
1292
|
+
name: "get_post",
|
|
1293
|
+
description: "Récupère un post avec tous ses champs (incluant meta brute + champs ACF formatés + taxonomies + featured image URL).",
|
|
1294
|
+
inputSchema: {
|
|
1295
|
+
type: "object",
|
|
1296
|
+
properties: {
|
|
1297
|
+
postId: { type: "number" },
|
|
1298
|
+
},
|
|
1299
|
+
required: ["postId"],
|
|
1300
|
+
},
|
|
1301
|
+
},
|
|
1302
|
+
{
|
|
1303
|
+
name: "list_posts",
|
|
1304
|
+
description: "Liste les posts d'un type donné avec filtres taxonomie/meta/search/pagination/order. Pour les Query Loops Bricks et inventaires.",
|
|
1305
|
+
inputSchema: {
|
|
1306
|
+
type: "object",
|
|
1307
|
+
properties: {
|
|
1308
|
+
postType: { type: "string" },
|
|
1309
|
+
perPage: { type: "number", description: "Défaut 20, max 100" },
|
|
1310
|
+
page: { type: "number", description: "Défaut 1" },
|
|
1311
|
+
search: { type: "string" },
|
|
1312
|
+
status: { type: "string", description: "Défaut 'publish'" },
|
|
1313
|
+
taxonomyFilter: { type: "object", description: "Format {taxonomy_slug: 'term-slug'}" },
|
|
1314
|
+
metaQuery: { type: "array", description: "WP_Query meta_query compatible" },
|
|
1315
|
+
orderBy: { type: "string", enum: ["date", "title", "menu_order", "meta_value"], description: "Défaut 'date'" },
|
|
1316
|
+
order: { type: "string", enum: ["ASC", "DESC"], description: "Défaut 'DESC'" },
|
|
1317
|
+
},
|
|
1318
|
+
required: ["postType"],
|
|
1319
|
+
},
|
|
1320
|
+
},
|
|
1321
|
+
{
|
|
1322
|
+
name: "create_taxonomy_term",
|
|
1323
|
+
description: "Crée un terme de taxonomie (ex: catégorie 'Salle de bain' dans 'categorie_chantier'). Idempotent : si terme existe déjà avec ce slug/nom, retourne son ID au lieu d'erreurer.",
|
|
1324
|
+
inputSchema: {
|
|
1325
|
+
type: "object",
|
|
1326
|
+
properties: {
|
|
1327
|
+
taxonomy: { type: "string", description: "Slug de la taxonomy (ex: 'categorie_chantier')" },
|
|
1328
|
+
name: { type: "string", description: "Nom du terme (ex: 'Salle de bain')" },
|
|
1329
|
+
slug: { type: "string", description: "Slug URL (auto-généré si omis)" },
|
|
1330
|
+
description: { type: "string" },
|
|
1331
|
+
parentId: { type: "number", description: "ID du terme parent (pour taxos hiérarchiques)" },
|
|
1332
|
+
},
|
|
1333
|
+
required: ["taxonomy", "name"],
|
|
1334
|
+
},
|
|
1335
|
+
},
|
|
1336
|
+
|
|
1105
1337
|
// ===== v3.7.0 — SKILL VERSIONING =====
|
|
1106
1338
|
{
|
|
1107
1339
|
name: "check_skill_version",
|
|
@@ -1551,260 +1783,393 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1551
1783
|
result = await callWordPressAPI("/list-components", "GET");
|
|
1552
1784
|
break;
|
|
1553
1785
|
|
|
1554
|
-
// ===== v3.
|
|
1786
|
+
// ===== v3.10 — VERIFY ELEMENT (multi-viewport + checks pluggables) =====
|
|
1555
1787
|
case "verify_element": {
|
|
1556
1788
|
const pageId = args.pageId;
|
|
1557
1789
|
const elementId = args.elementId;
|
|
1558
|
-
|
|
1790
|
+
// Backward compat : `viewport` (singulier) OU `viewports` (array)
|
|
1791
|
+
const viewportList = (Array.isArray(args.viewports) && args.viewports.length > 0)
|
|
1792
|
+
? args.viewports
|
|
1793
|
+
: [args.viewport || "desktop"];
|
|
1794
|
+
const isMulti = viewportList.length > 1;
|
|
1795
|
+
const checksConfig = Object.assign({
|
|
1796
|
+
expected_styles: true,
|
|
1797
|
+
sibling_coherence: true,
|
|
1798
|
+
empty_containers: true,
|
|
1799
|
+
media_health: true,
|
|
1800
|
+
overflow: true,
|
|
1801
|
+
console_errors: true,
|
|
1802
|
+
}, args.checks || {});
|
|
1559
1803
|
const viewportSizes = {
|
|
1560
1804
|
desktop: { width: 1920, height: 1080 },
|
|
1561
1805
|
tablet: { width: 991, height: 1200 },
|
|
1562
1806
|
mobile_landscape: { width: 767, height: 600 },
|
|
1563
1807
|
mobile_portrait: { width: 478, height: 800 },
|
|
1564
1808
|
};
|
|
1565
|
-
const viewportContext = viewportSizes[viewport] || viewportSizes.desktop;
|
|
1566
1809
|
|
|
1567
|
-
// 1) Récupérer infos plugin (URL, sélecteur, expected styles)
|
|
1810
|
+
// 1) Récupérer infos plugin une seule fois (URL, sélecteur, expected styles)
|
|
1568
1811
|
const info = await callWordPressAPI("/verify-element-info", "POST", { pageId, elementId });
|
|
1569
1812
|
|
|
1570
|
-
// 2)
|
|
1571
|
-
|
|
1572
|
-
try {
|
|
1573
|
-
page = await getNewPage(viewport);
|
|
1574
|
-
} catch (browserErr) {
|
|
1575
|
-
// Erreur Playwright (chromium pas installé etc) — retourner sans crasher
|
|
1576
|
-
result = {
|
|
1577
|
-
success: false,
|
|
1578
|
-
error: browserErr.message,
|
|
1579
|
-
url: info.url,
|
|
1580
|
-
selector: info.selector,
|
|
1581
|
-
hint: "Installation Chromium requise pour verify_element. À défaut, utilise screenshot-website-fast en MCP externe.",
|
|
1582
|
-
};
|
|
1583
|
-
break;
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
// Collecte console errors
|
|
1587
|
-
const consoleErrors = [];
|
|
1588
|
-
page.on('console', msg => {
|
|
1589
|
-
if (msg.type() === 'error') consoleErrors.push(msg.text());
|
|
1590
|
-
});
|
|
1591
|
-
page.on('pageerror', err => consoleErrors.push('PageError: ' + err.message));
|
|
1813
|
+
// 2) Boucler sur chaque viewport — chacun = 1 page, 1 screenshot, 1 report
|
|
1814
|
+
const perViewport = [];
|
|
1592
1815
|
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
let computed = null;
|
|
1596
|
-
let loadedFonts = [];
|
|
1816
|
+
for (const viewport of viewportList) {
|
|
1817
|
+
const viewportContext = viewportSizes[viewport] || viewportSizes.desktop;
|
|
1597
1818
|
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
if (!element) {
|
|
1605
|
-
// Diagnostic : qu'a-t-on réellement chargé ?
|
|
1606
|
-
const diagnostics = await page.evaluate(() => ({
|
|
1607
|
-
actualUrl: window.location.href,
|
|
1608
|
-
title: document.title,
|
|
1609
|
-
bodyClass: document.body?.className || '',
|
|
1610
|
-
brxeCount: document.querySelectorAll('[class*="brxe-"]').length,
|
|
1611
|
-
firstBrxe: document.querySelector('[class*="brxe-"]')?.className || null,
|
|
1612
|
-
htmlSnippet: document.documentElement.outerHTML.substring(0, 500),
|
|
1613
|
-
}));
|
|
1614
|
-
result = {
|
|
1819
|
+
let page;
|
|
1820
|
+
try {
|
|
1821
|
+
page = await getNewPage(viewport);
|
|
1822
|
+
} catch (browserErr) {
|
|
1823
|
+
perViewport.push({
|
|
1824
|
+
viewport,
|
|
1615
1825
|
success: false,
|
|
1616
|
-
error:
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
hint: diagnostics.brxeCount === 0
|
|
1621
|
-
? "Aucune classe .brxe-* trouvée — la page n'a probablement pas chargé le contenu Bricks (cert SSL, redirect, 404, page d'erreur)"
|
|
1622
|
-
: `${diagnostics.brxeCount} éléments .brxe-* trouvés mais pas ${info.selector} — l'ID dans la DB ne correspond pas au rendu`,
|
|
1623
|
-
};
|
|
1624
|
-
await page.close();
|
|
1625
|
-
break;
|
|
1826
|
+
error: browserErr.message,
|
|
1827
|
+
hint: "Installation Chromium requise pour verify_element. À défaut, utilise screenshot-website-fast en MCP externe.",
|
|
1828
|
+
});
|
|
1829
|
+
continue;
|
|
1626
1830
|
}
|
|
1627
1831
|
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
// Computed style + dimensions
|
|
1632
|
-
computed = await element.evaluate(el => {
|
|
1633
|
-
const cs = getComputedStyle(el);
|
|
1634
|
-
const rect = el.getBoundingClientRect();
|
|
1635
|
-
// Enfants "réels" = ceux qui ont une classe brxe-{id} ou id brxe-*
|
|
1636
|
-
// Exclut les <video>, <picture> et autres ajoutés par Bricks (bg vidéo, overlay auto, etc.)
|
|
1637
|
-
const realChildren = Array.from(el.children).filter(child => {
|
|
1638
|
-
if (child.id && child.id.startsWith('brxe-')) return true;
|
|
1639
|
-
return Array.from(child.classList).some(cls => cls.startsWith('brxe-'));
|
|
1640
|
-
});
|
|
1641
|
-
const bricksInternalChildren = Array.from(el.children).filter(child => {
|
|
1642
|
-
return !((child.id && child.id.startsWith('brxe-')) ||
|
|
1643
|
-
Array.from(child.classList).some(cls => cls.startsWith('brxe-')));
|
|
1644
|
-
}).map(c => c.tagName.toLowerCase() + (c.className ? '.' + c.className.split(' ').join('.') : ''));
|
|
1645
|
-
return {
|
|
1646
|
-
display: cs.display,
|
|
1647
|
-
'flex-direction': cs.flexDirection,
|
|
1648
|
-
'justify-content': cs.justifyContent,
|
|
1649
|
-
'align-items': cs.alignItems,
|
|
1650
|
-
gap: cs.gap,
|
|
1651
|
-
'column-gap': cs.columnGap,
|
|
1652
|
-
'row-gap': cs.rowGap,
|
|
1653
|
-
width: Math.round(rect.width) + 'px',
|
|
1654
|
-
height: Math.round(rect.height) + 'px',
|
|
1655
|
-
'max-width': cs.maxWidth,
|
|
1656
|
-
'padding-top': cs.paddingTop,
|
|
1657
|
-
'padding-right': cs.paddingRight,
|
|
1658
|
-
'padding-bottom': cs.paddingBottom,
|
|
1659
|
-
'padding-left': cs.paddingLeft,
|
|
1660
|
-
'margin-top': cs.marginTop,
|
|
1661
|
-
'margin-right': cs.marginRight,
|
|
1662
|
-
'margin-bottom': cs.marginBottom,
|
|
1663
|
-
'margin-left': cs.marginLeft,
|
|
1664
|
-
'background-color': cs.backgroundColor,
|
|
1665
|
-
'font-size': cs.fontSize,
|
|
1666
|
-
'font-family': cs.fontFamily,
|
|
1667
|
-
'font-weight': cs.fontWeight,
|
|
1668
|
-
'line-height': cs.lineHeight,
|
|
1669
|
-
color: cs.color,
|
|
1670
|
-
'text-align': cs.textAlign,
|
|
1671
|
-
'border-top-left-radius': cs.borderTopLeftRadius,
|
|
1672
|
-
'border-top-right-radius': cs.borderTopRightRadius,
|
|
1673
|
-
'border-bottom-right-radius': cs.borderBottomRightRadius,
|
|
1674
|
-
'border-bottom-left-radius': cs.borderBottomLeftRadius,
|
|
1675
|
-
visibility: cs.visibility,
|
|
1676
|
-
opacity: cs.opacity,
|
|
1677
|
-
childrenInDom: realChildren.length,
|
|
1678
|
-
childrenTotalDom: el.children.length,
|
|
1679
|
-
bricksInternalChildren: bricksInternalChildren,
|
|
1680
|
-
isVisible: rect.width > 0 && rect.height > 0 && cs.visibility !== 'hidden' && cs.opacity !== '0',
|
|
1681
|
-
hasOverflowX: el.scrollWidth > el.clientWidth,
|
|
1682
|
-
};
|
|
1832
|
+
const consoleErrors = [];
|
|
1833
|
+
page.on('console', msg => {
|
|
1834
|
+
if (msg.type() === 'error') consoleErrors.push(msg.text());
|
|
1683
1835
|
});
|
|
1836
|
+
page.on('pageerror', err => consoleErrors.push('PageError: ' + err.message));
|
|
1684
1837
|
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
document.fonts.forEach(f => { if (f.status === 'loaded') set.add(f.family); });
|
|
1689
|
-
return Array.from(set);
|
|
1690
|
-
});
|
|
1838
|
+
let screenshotBase64 = null;
|
|
1839
|
+
let audit = null;
|
|
1840
|
+
let loadedFonts = [];
|
|
1691
1841
|
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1842
|
+
try {
|
|
1843
|
+
await page.goto(info.url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
1844
|
+
await page.waitForTimeout(2000);
|
|
1845
|
+
|
|
1846
|
+
const element = await page.$(info.selector);
|
|
1847
|
+
if (!element) {
|
|
1848
|
+
const diagnostics = await page.evaluate(() => ({
|
|
1849
|
+
actualUrl: window.location.href,
|
|
1850
|
+
title: document.title,
|
|
1851
|
+
bodyClass: document.body?.className || '',
|
|
1852
|
+
brxeCount: document.querySelectorAll('[class*="brxe-"]').length,
|
|
1853
|
+
firstBrxe: document.querySelector('[class*="brxe-"]')?.className || null,
|
|
1854
|
+
htmlSnippet: document.documentElement.outerHTML.substring(0, 500),
|
|
1855
|
+
}));
|
|
1856
|
+
perViewport.push({
|
|
1857
|
+
viewport,
|
|
1858
|
+
success: false,
|
|
1859
|
+
error: `Élément ${info.selector} introuvable dans le DOM`,
|
|
1860
|
+
diagnostics,
|
|
1861
|
+
hint: diagnostics.brxeCount === 0
|
|
1862
|
+
? "Aucune classe .brxe-* trouvée — la page n'a probablement pas chargé le contenu Bricks (cert SSL, redirect, 404, page d'erreur)"
|
|
1863
|
+
: `${diagnostics.brxeCount} éléments .brxe-* trouvés mais pas ${info.selector} — l'ID dans la DB ne correspond pas au rendu`,
|
|
1864
|
+
});
|
|
1865
|
+
continue;
|
|
1866
|
+
}
|
|
1700
1867
|
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1868
|
+
await element.scrollIntoViewIfNeeded();
|
|
1869
|
+
await page.waitForTimeout(400);
|
|
1870
|
+
|
|
1871
|
+
// === v3.10 : collecte enrichie en 1 evaluate (self + siblings + media + emptyContainers) ===
|
|
1872
|
+
audit = await page.evaluate((selector) => {
|
|
1873
|
+
const el = document.querySelector(selector);
|
|
1874
|
+
if (!el) return null;
|
|
1875
|
+
const cs = getComputedStyle(el);
|
|
1876
|
+
const rect = el.getBoundingClientRect();
|
|
1877
|
+
|
|
1878
|
+
const realChildren = Array.from(el.children).filter(child => {
|
|
1879
|
+
if (child.id && child.id.startsWith('brxe-')) return true;
|
|
1880
|
+
return Array.from(child.classList).some(cls => cls.startsWith('brxe-'));
|
|
1881
|
+
});
|
|
1882
|
+
const bricksInternalChildren = Array.from(el.children).filter(child => {
|
|
1883
|
+
return !((child.id && child.id.startsWith('brxe-')) ||
|
|
1884
|
+
Array.from(child.classList).some(cls => cls.startsWith('brxe-')));
|
|
1885
|
+
}).map(c => c.tagName.toLowerCase() + (c.className ? '.' + c.className.split(' ').filter(Boolean).join('.') : ''));
|
|
1886
|
+
|
|
1887
|
+
const computed = {
|
|
1888
|
+
display: cs.display,
|
|
1889
|
+
'flex-direction': cs.flexDirection,
|
|
1890
|
+
'justify-content': cs.justifyContent,
|
|
1891
|
+
'align-items': cs.alignItems,
|
|
1892
|
+
gap: cs.gap,
|
|
1893
|
+
'column-gap': cs.columnGap,
|
|
1894
|
+
'row-gap': cs.rowGap,
|
|
1895
|
+
width: Math.round(rect.width) + 'px',
|
|
1896
|
+
height: Math.round(rect.height) + 'px',
|
|
1897
|
+
'max-width': cs.maxWidth,
|
|
1898
|
+
'padding-top': cs.paddingTop,
|
|
1899
|
+
'padding-right': cs.paddingRight,
|
|
1900
|
+
'padding-bottom': cs.paddingBottom,
|
|
1901
|
+
'padding-left': cs.paddingLeft,
|
|
1902
|
+
'margin-top': cs.marginTop,
|
|
1903
|
+
'margin-right': cs.marginRight,
|
|
1904
|
+
'margin-bottom': cs.marginBottom,
|
|
1905
|
+
'margin-left': cs.marginLeft,
|
|
1906
|
+
'background-color': cs.backgroundColor,
|
|
1907
|
+
'font-size': cs.fontSize,
|
|
1908
|
+
'font-family': cs.fontFamily,
|
|
1909
|
+
'font-weight': cs.fontWeight,
|
|
1910
|
+
'line-height': cs.lineHeight,
|
|
1911
|
+
color: cs.color,
|
|
1912
|
+
'text-align': cs.textAlign,
|
|
1913
|
+
'border-top-left-radius': cs.borderTopLeftRadius,
|
|
1914
|
+
'border-top-right-radius': cs.borderTopRightRadius,
|
|
1915
|
+
'border-bottom-right-radius': cs.borderBottomRightRadius,
|
|
1916
|
+
'border-bottom-left-radius': cs.borderBottomLeftRadius,
|
|
1917
|
+
visibility: cs.visibility,
|
|
1918
|
+
opacity: cs.opacity,
|
|
1919
|
+
childrenInDom: realChildren.length,
|
|
1920
|
+
childrenTotalDom: el.children.length,
|
|
1921
|
+
bricksInternalChildren,
|
|
1922
|
+
isVisible: rect.width > 0 && rect.height > 0 && cs.visibility !== 'hidden' && cs.opacity !== '0',
|
|
1923
|
+
hasOverflowX: el.scrollWidth > el.clientWidth,
|
|
1924
|
+
};
|
|
1925
|
+
|
|
1926
|
+
// SIBLINGS (frères directs Bricks)
|
|
1927
|
+
const self = {
|
|
1928
|
+
id: el.id || '',
|
|
1929
|
+
'text-align': cs.textAlign,
|
|
1930
|
+
'font-size': cs.fontSize,
|
|
1931
|
+
'font-size-px': parseFloat(cs.fontSize),
|
|
1932
|
+
};
|
|
1933
|
+
let siblings = [];
|
|
1934
|
+
if (el.parentElement) {
|
|
1935
|
+
const realSiblings = Array.from(el.parentElement.children).filter(s => {
|
|
1936
|
+
if (s === el) return false;
|
|
1937
|
+
if (s.id && s.id.startsWith('brxe-')) return true;
|
|
1938
|
+
return Array.from(s.classList).some(c => c.startsWith('brxe-'));
|
|
1939
|
+
});
|
|
1940
|
+
siblings = realSiblings.map(s => {
|
|
1941
|
+
const scs = getComputedStyle(s);
|
|
1942
|
+
const srect = s.getBoundingClientRect();
|
|
1943
|
+
return {
|
|
1944
|
+
id: s.id || '',
|
|
1945
|
+
classes: Array.from(s.classList).filter(c => c.startsWith('brxe-')),
|
|
1946
|
+
'text-align': scs.textAlign,
|
|
1947
|
+
'font-size': scs.fontSize,
|
|
1948
|
+
'font-size-px': parseFloat(scs.fontSize),
|
|
1949
|
+
rect: { x: Math.round(srect.x), y: Math.round(srect.y), w: Math.round(srect.width), h: Math.round(srect.height) },
|
|
1950
|
+
};
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1710
1953
|
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1954
|
+
// MEDIA (img + video à l'intérieur de l'élément)
|
|
1955
|
+
const images = Array.from(el.querySelectorAll('img')).map(img => ({
|
|
1956
|
+
src: img.currentSrc || img.src || '',
|
|
1957
|
+
naturalWidth: img.naturalWidth,
|
|
1958
|
+
naturalHeight: img.naturalHeight,
|
|
1959
|
+
alt: img.alt || '',
|
|
1960
|
+
loaded: img.naturalWidth > 0,
|
|
1961
|
+
loading: img.getAttribute('loading') || 'eager',
|
|
1962
|
+
}));
|
|
1963
|
+
const videos = Array.from(el.querySelectorAll('video')).map(v => ({
|
|
1964
|
+
src: v.currentSrc || v.src || '',
|
|
1965
|
+
readyState: v.readyState,
|
|
1966
|
+
paused: v.paused,
|
|
1967
|
+
loaded: v.readyState >= 2,
|
|
1968
|
+
}));
|
|
1969
|
+
|
|
1970
|
+
// EMPTY CONTAINERS (descendants Bricks visibles ≥ 50×50px sans contenu)
|
|
1971
|
+
const allBrxe = Array.from(el.querySelectorAll('[id^="brxe-"], [class*="brxe-"]'));
|
|
1972
|
+
const emptyContainers = [];
|
|
1973
|
+
const MIN_AREA = 2500;
|
|
1974
|
+
allBrxe.forEach(b => {
|
|
1975
|
+
const brect = b.getBoundingClientRect();
|
|
1976
|
+
if (brect.width < 50 || brect.height < 50) return;
|
|
1977
|
+
const area = brect.width * brect.height;
|
|
1978
|
+
if (area < MIN_AREA) return;
|
|
1979
|
+
const hasText = (b.textContent || '').trim().length > 0;
|
|
1980
|
+
const hasMedia = b.querySelector('img, picture, svg, video, iframe') !== null;
|
|
1981
|
+
const hasInteractive = b.querySelector('a, button, input, select, textarea') !== null;
|
|
1982
|
+
const bcs = getComputedStyle(b);
|
|
1983
|
+
const hasBgImg = bcs.backgroundImage && bcs.backgroundImage !== 'none';
|
|
1984
|
+
if (!hasText && !hasMedia && !hasInteractive && !hasBgImg) {
|
|
1985
|
+
emptyContainers.push({
|
|
1986
|
+
id: b.id || '',
|
|
1987
|
+
classes: Array.from(b.classList).filter(c => c.startsWith('brxe-')),
|
|
1988
|
+
rect: { x: Math.round(brect.x), y: Math.round(brect.y), w: Math.round(brect.width), h: Math.round(brect.height) },
|
|
1989
|
+
area: Math.round(area),
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1992
|
+
});
|
|
1726
1993
|
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
e.includes('429') || e.includes('net::ERR_') || e.includes('Failed to load resource')
|
|
1736
|
-
);
|
|
1737
|
-
|
|
1738
|
-
if (realErrors.length > 0) {
|
|
1739
|
-
checks.push({
|
|
1740
|
-
ok: false,
|
|
1741
|
-
label: `${realErrors.length} erreur(s) JS dans la console`,
|
|
1742
|
-
got: realErrors.slice(0, 3),
|
|
1743
|
-
hint: "Erreur JavaScript détectée — pas lié au serveur",
|
|
1744
|
-
});
|
|
1745
|
-
} else {
|
|
1746
|
-
checks.push({ ok: true, label: "Aucune erreur JS console" });
|
|
1747
|
-
}
|
|
1748
|
-
// Erreurs réseau : info seulement, pas un échec
|
|
1749
|
-
if (networkErrors.length > 0) {
|
|
1750
|
-
checks.push({
|
|
1751
|
-
ok: true,
|
|
1752
|
-
label: `${networkErrors.length} erreur(s) réseau (429/load) — non bloquant`,
|
|
1753
|
-
got: networkErrors.slice(0, 2),
|
|
1994
|
+
return { computed, self, siblings, media: { images, videos }, emptyContainers };
|
|
1995
|
+
}, info.selector);
|
|
1996
|
+
|
|
1997
|
+
// Fonts loaded
|
|
1998
|
+
loadedFonts = await page.evaluate(() => {
|
|
1999
|
+
const set = new Set();
|
|
2000
|
+
document.fonts.forEach(f => { if (f.status === 'loaded') set.add(f.family); });
|
|
2001
|
+
return Array.from(set);
|
|
1754
2002
|
});
|
|
1755
|
-
}
|
|
1756
2003
|
|
|
1757
|
-
|
|
1758
|
-
|
|
2004
|
+
// === Construction des checks ===
|
|
2005
|
+
const checks = [];
|
|
2006
|
+
const computed = audit.computed;
|
|
2007
|
+
checks.push({ ok: true, label: `Élément trouvé dans le DOM (${info.selector})` });
|
|
1759
2008
|
checks.push({
|
|
1760
|
-
ok:
|
|
1761
|
-
label:
|
|
1762
|
-
hint: "
|
|
2009
|
+
ok: computed.isVisible,
|
|
2010
|
+
label: `Élément visible (${computed.width} × ${computed.height})`,
|
|
2011
|
+
...(computed.isVisible ? {} : { severity: 'critical', hint: "Largeur ou hauteur à 0 — vérifie les enfants ou le padding du parent" })
|
|
1763
2012
|
});
|
|
1764
|
-
}
|
|
1765
2013
|
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
}
|
|
2014
|
+
if (typeof info.childrenCount === 'number') {
|
|
2015
|
+
const ok = info.childrenCount === computed.childrenInDom;
|
|
2016
|
+
checks.push({
|
|
2017
|
+
ok,
|
|
2018
|
+
label: `${info.childrenCount} enfant(s) attendu(s) → ${computed.childrenInDom} dans le DOM`,
|
|
2019
|
+
...(ok ? {} : { severity: 'warning', expected: info.childrenCount, got: computed.childrenInDom })
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
1775
2022
|
|
|
1776
|
-
|
|
1777
|
-
|
|
2023
|
+
// Expected styles (viewport-aware vh/vw)
|
|
2024
|
+
if (checksConfig.expected_styles) {
|
|
2025
|
+
const expected = info.expected || {};
|
|
2026
|
+
for (const [prop, expectedVal] of Object.entries(expected)) {
|
|
2027
|
+
const got = computed[prop];
|
|
2028
|
+
if (got === undefined) continue;
|
|
2029
|
+
const ok = normaliseCssValue(got, viewportContext) === normaliseCssValue(expectedVal, viewportContext);
|
|
2030
|
+
const check = { ok, label: `${prop} = ${expectedVal}` };
|
|
2031
|
+
if (!ok) {
|
|
2032
|
+
check.severity = 'warning';
|
|
2033
|
+
check.expected = expectedVal;
|
|
2034
|
+
check.got = got;
|
|
2035
|
+
const hint = generateHint(prop, expectedVal, got);
|
|
2036
|
+
if (hint) check.hint = hint;
|
|
2037
|
+
}
|
|
2038
|
+
checks.push(check);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
1778
2041
|
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
2042
|
+
// v3.10 : cohérence siblings
|
|
2043
|
+
if (checksConfig.sibling_coherence) {
|
|
2044
|
+
checks.push(...buildSiblingCoherenceChecks(audit));
|
|
2045
|
+
}
|
|
2046
|
+
// v3.10 : containers vides
|
|
2047
|
+
if (checksConfig.empty_containers) {
|
|
2048
|
+
checks.push(...buildEmptyContainerChecks(audit));
|
|
2049
|
+
}
|
|
2050
|
+
// v3.10 : santé médias
|
|
2051
|
+
if (checksConfig.media_health) {
|
|
2052
|
+
checks.push(...buildMediaHealthChecks(audit));
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
// Console errors : séparer JS vs réseau (429/timeout) — réseau = info, JS = échec
|
|
2056
|
+
if (checksConfig.console_errors) {
|
|
2057
|
+
const realErrors = consoleErrors.filter(e =>
|
|
2058
|
+
!e.includes('429') && !e.includes('net::ERR_') && !e.includes('Failed to load resource')
|
|
2059
|
+
);
|
|
2060
|
+
const networkErrors = consoleErrors.filter(e =>
|
|
2061
|
+
e.includes('429') || e.includes('net::ERR_') || e.includes('Failed to load resource')
|
|
2062
|
+
);
|
|
2063
|
+
if (realErrors.length > 0) {
|
|
2064
|
+
checks.push({
|
|
2065
|
+
ok: false,
|
|
2066
|
+
severity: 'critical',
|
|
2067
|
+
label: `${realErrors.length} erreur(s) JS dans la console`,
|
|
2068
|
+
got: realErrors.slice(0, 3),
|
|
2069
|
+
hint: "Erreur JavaScript détectée — pas lié au serveur",
|
|
2070
|
+
});
|
|
2071
|
+
} else {
|
|
2072
|
+
checks.push({ ok: true, label: "Aucune erreur JS console" });
|
|
2073
|
+
}
|
|
2074
|
+
if (networkErrors.length > 0) {
|
|
2075
|
+
checks.push({
|
|
2076
|
+
ok: true,
|
|
2077
|
+
severity: 'info',
|
|
2078
|
+
label: `${networkErrors.length} erreur(s) réseau (429/load) — non bloquant`,
|
|
2079
|
+
got: networkErrors.slice(0, 2),
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// Overflow horizontal
|
|
2085
|
+
if (checksConfig.overflow && computed.hasOverflowX) {
|
|
2086
|
+
checks.push({
|
|
2087
|
+
ok: false,
|
|
2088
|
+
severity: 'warning',
|
|
2089
|
+
label: "Débordement horizontal détecté",
|
|
2090
|
+
hint: "Un enfant dépasse la largeur du conteneur — vérifie les _widthMax et white-space",
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
// Screenshot crop (fallback fullpage si l'élément ne peut pas être capturé seul)
|
|
2095
|
+
try {
|
|
2096
|
+
const buf = await element.screenshot({ type: 'png' });
|
|
2097
|
+
screenshotBase64 = buf.toString('base64');
|
|
2098
|
+
} catch (e) {
|
|
2099
|
+
const buf = await page.screenshot({ type: 'png', fullPage: false });
|
|
2100
|
+
screenshotBase64 = buf.toString('base64');
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
const okCount = checks.filter(c => c.ok).length;
|
|
2104
|
+
perViewport.push({
|
|
2105
|
+
viewport,
|
|
2106
|
+
success: true,
|
|
2107
|
+
report: { score: `${okCount}/${checks.length}`, checks },
|
|
2108
|
+
computed,
|
|
2109
|
+
loadedFonts,
|
|
2110
|
+
screenshotBase64,
|
|
2111
|
+
});
|
|
2112
|
+
} finally {
|
|
2113
|
+
if (page && !page.isClosed()) {
|
|
2114
|
+
await page.close().catch(() => {});
|
|
2115
|
+
}
|
|
1794
2116
|
}
|
|
1795
2117
|
}
|
|
1796
2118
|
|
|
1797
|
-
//
|
|
1798
|
-
const
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
2119
|
+
// 3) Construction de la réponse MCP
|
|
2120
|
+
const baseInfo = {
|
|
2121
|
+
url: info.url,
|
|
2122
|
+
urlWithAnchor: info.urlWithAnchor,
|
|
2123
|
+
selector: info.selector,
|
|
2124
|
+
name: info.name,
|
|
2125
|
+
label: info.label,
|
|
2126
|
+
};
|
|
2127
|
+
const responseContent = [];
|
|
2128
|
+
|
|
2129
|
+
if (isMulti) {
|
|
2130
|
+
// Multi-viewport : 1 JSON consolidé + 1 image par viewport (les b64 sont retirés du JSON)
|
|
2131
|
+
const summary = {
|
|
2132
|
+
...baseInfo,
|
|
2133
|
+
viewports: perViewport.map(vr => {
|
|
2134
|
+
const { screenshotBase64: _drop, ...rest } = vr;
|
|
2135
|
+
return rest;
|
|
2136
|
+
}),
|
|
2137
|
+
};
|
|
2138
|
+
responseContent.push({ type: "text", text: JSON.stringify(summary, null, 2) });
|
|
2139
|
+
for (const vr of perViewport) {
|
|
2140
|
+
if (vr.screenshotBase64) {
|
|
2141
|
+
responseContent.push({ type: "image", data: vr.screenshotBase64, mimeType: "image/png" });
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
} else {
|
|
2145
|
+
// Single viewport : structure plate (backward compat)
|
|
2146
|
+
const vr = perViewport[0];
|
|
2147
|
+
let flatResult;
|
|
2148
|
+
if (vr.success) {
|
|
2149
|
+
flatResult = {
|
|
2150
|
+
success: true,
|
|
2151
|
+
...baseInfo,
|
|
2152
|
+
viewport: vr.viewport,
|
|
2153
|
+
report: vr.report,
|
|
2154
|
+
computed: vr.computed,
|
|
2155
|
+
loadedFonts: vr.loadedFonts,
|
|
2156
|
+
};
|
|
2157
|
+
} else {
|
|
2158
|
+
flatResult = {
|
|
2159
|
+
success: false,
|
|
2160
|
+
...baseInfo,
|
|
2161
|
+
viewport: vr.viewport,
|
|
2162
|
+
error: vr.error,
|
|
2163
|
+
diagnostics: vr.diagnostics,
|
|
2164
|
+
hint: vr.hint,
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
responseContent.push({ type: "text", text: JSON.stringify(flatResult, null, 2) });
|
|
2168
|
+
if (vr.screenshotBase64) {
|
|
2169
|
+
responseContent.push({ type: "image", data: vr.screenshotBase64, mimeType: "image/png" });
|
|
2170
|
+
}
|
|
1807
2171
|
}
|
|
2172
|
+
|
|
1808
2173
|
return { content: responseContent };
|
|
1809
2174
|
}
|
|
1810
2175
|
|
|
@@ -1847,6 +2212,79 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1847
2212
|
break;
|
|
1848
2213
|
}
|
|
1849
2214
|
|
|
2215
|
+
// ===== v3.9.0 — CUSTOM POST TYPES =====
|
|
2216
|
+
case "list_post_types":
|
|
2217
|
+
result = await callWordPressAPI("/list-post-types", "GET");
|
|
2218
|
+
break;
|
|
2219
|
+
|
|
2220
|
+
case "create_post":
|
|
2221
|
+
result = await callWordPressAPI("/create-post", "POST", {
|
|
2222
|
+
postType: args.postType,
|
|
2223
|
+
title: args.title,
|
|
2224
|
+
content: args.content,
|
|
2225
|
+
excerpt: args.excerpt,
|
|
2226
|
+
slug: args.slug,
|
|
2227
|
+
status: args.status,
|
|
2228
|
+
featuredImageId: args.featuredImageId,
|
|
2229
|
+
meta: args.meta,
|
|
2230
|
+
taxonomies: args.taxonomies,
|
|
2231
|
+
date: args.date,
|
|
2232
|
+
author: args.author,
|
|
2233
|
+
});
|
|
2234
|
+
break;
|
|
2235
|
+
|
|
2236
|
+
case "update_post":
|
|
2237
|
+
result = await callWordPressAPI("/update-post", "POST", {
|
|
2238
|
+
postId: args.postId,
|
|
2239
|
+
title: args.title,
|
|
2240
|
+
content: args.content,
|
|
2241
|
+
excerpt: args.excerpt,
|
|
2242
|
+
slug: args.slug,
|
|
2243
|
+
status: args.status,
|
|
2244
|
+
featuredImageId: args.featuredImageId,
|
|
2245
|
+
meta: args.meta,
|
|
2246
|
+
taxonomies: args.taxonomies,
|
|
2247
|
+
date: args.date,
|
|
2248
|
+
});
|
|
2249
|
+
break;
|
|
2250
|
+
|
|
2251
|
+
case "delete_post":
|
|
2252
|
+
result = await callWordPressAPI("/delete-post", "POST", {
|
|
2253
|
+
postId: args.postId,
|
|
2254
|
+
force: args.force,
|
|
2255
|
+
});
|
|
2256
|
+
break;
|
|
2257
|
+
|
|
2258
|
+
case "get_post":
|
|
2259
|
+
result = await callWordPressAPI("/get-post", "POST", {
|
|
2260
|
+
postId: args.postId,
|
|
2261
|
+
});
|
|
2262
|
+
break;
|
|
2263
|
+
|
|
2264
|
+
case "list_posts":
|
|
2265
|
+
result = await callWordPressAPI("/list-posts", "POST", {
|
|
2266
|
+
postType: args.postType,
|
|
2267
|
+
perPage: args.perPage,
|
|
2268
|
+
page: args.page,
|
|
2269
|
+
search: args.search,
|
|
2270
|
+
status: args.status,
|
|
2271
|
+
taxonomyFilter: args.taxonomyFilter,
|
|
2272
|
+
metaQuery: args.metaQuery,
|
|
2273
|
+
orderBy: args.orderBy,
|
|
2274
|
+
order: args.order,
|
|
2275
|
+
});
|
|
2276
|
+
break;
|
|
2277
|
+
|
|
2278
|
+
case "create_taxonomy_term":
|
|
2279
|
+
result = await callWordPressAPI("/create-taxonomy-term", "POST", {
|
|
2280
|
+
taxonomy: args.taxonomy,
|
|
2281
|
+
name: args.name,
|
|
2282
|
+
slug: args.slug,
|
|
2283
|
+
description: args.description,
|
|
2284
|
+
parentId: args.parentId,
|
|
2285
|
+
});
|
|
2286
|
+
break;
|
|
2287
|
+
|
|
1850
2288
|
// ===== v3.7.0 — UPLOAD FROM LOCAL FILESYSTEM =====
|
|
1851
2289
|
case "upload_local_file": {
|
|
1852
2290
|
try {
|