Oui — dans WeWeb, le plus propre c’est : HTML/CSS dans un “Custom Code / HTML Embed” + JS dans un Workflow “On page load” via “Custom JavaScript action” (c’est la méthode recommandée pour exécuter du JS de manière fiable).
Pourquoi pas juste coller <script> dans le bloc HTML ?
Ça peut marcher, mais c’est plus fragile (timing de chargement, editor vs publié). WeWeb pousse justement le pattern Workflow → Custom JavaScript.
Intégration BODYNUT “Gap Block” dans WeWeb (pas à pas)
1) Récupérer tes données Supabase dans WeWeb
Active le plugin Supabase dans WeWeb (data source).
Crée des Collections (requêtes) :
targets(ex:macro_targetsactif +metabolism_settings+goalsactif)actuals(moyenne 7 jours :nutrition_logs,checkins,workout_sessions)
💡 Astuce pro : fais une VIEW côté Supabase (ex: dashboard_gap_view) qui te renvoie déjà targets + actuals + context en une requête → WeWeb adore ça (moins de logique dispersée).
2) Créer une variable “gapData” dans WeWeb
Dans WeWeb → Variables → crée une variable Object :
gapDataElle contiendra exactement ce que ton JS attend :
{ targets: {...}, actuals: {...}, context: {...} }
Ensuite :
Workflow “On page load” :
Action : Fetch ta collection (ou ta view)
Action : Set variable
gapData = result
3) Mettre le HTML/CSS du bloc dans la page
Dans l’éditeur WeWeb :
Ajoute un élément Custom Code (ou HTML Embed)
Colle uniquement :
le
<section …>...</section>le
<style>...</style>
⚠️ Ne colle pas le gros
<script>ici (on va le mettre dans Workflow).
Important :
garde un
idstable sur le root :id="bodynut-gap"
4) Mettre le JS dans un Workflow WeWeb
WeWeb → Page settings → Workflows → On page load :
Ajoute une action Custom JavaScript
Colle une version “fonction” du script (ci-dessous).
💡 Option bonus : crée aussi un Workflow “On variable change (gapData)” si tu veux que ça se mette à jour quand tu changes de date / profil.
Le JS “WeWeb-friendly” (à coller dans Custom JavaScript action)
👉 L’idée : on lit la variable gapData et on appelle renderGap(data).
// 1) Récupérer la variable WeWeb (remplace "gapData" par l'ID/nom exact si besoin)
const data = wwLib.wwVariable.getValue("gapData");
// 2) Sécurité
if (!data || !data.targets || !data.actuals) {
console.warn("BODYNUT Gap: data manquante", data);
return;
}
// 3) Render
renderBodynutGap(data, "bodynut-gap");
// ---- RENDERER (version compacte) ----
function renderBodynutGap(data, rootId){
const root = document.getElementById(rootId);
if(!root){ console.warn("BODYNUT Gap: root introuvable"); return; }
const clamp = (n,a,b)=>Math.max(a,Math.min(b,n));
function adherenceScore(actual,target,tolPct){
if(target==null||actual==null) return 50;
const tol = Math.abs(target)*(tolPct/100);
const diff = Math.abs(actual-target);
const raw = 100 - (diff/(tol||1))*100;
return clamp(raw,0,100);
}
const pillFromScore = s => (s>=85?"OK":s>=70?"Bien":s>=55?"À corriger":"Priorité");
const formatDelta = (a,t,u="")=>{
const d=a-t; const sign=d>0?"+":"";
return `${sign}${Math.round(d)}${u}`;
};
const scores = {
kcal: adherenceScore(data.actuals.kcal, data.targets.kcal, 7),
protein: adherenceScore(data.actuals.protein_g, data.targets.protein_g, 8),
neat: adherenceScore(data.actuals.steps, data.targets.steps, 12),
training: adherenceScore(data.actuals.training_sessions_per_week, data.targets.training_sessions_per_week, 15),
sleep: adherenceScore(data.actuals.sleep_hours, data.targets.sleep_hours, 10),
quality: clamp(data.actuals.micro_score ?? 50, 0, 100),
};
const weights = { kcal:0.22, protein:0.22, neat:0.18, training:0.16, sleep:0.12, quality:0.10 };
const recompScore = Math.round(Object.keys(weights).reduce((acc,k)=>acc+scores[k]*weights[k],0));
const order = ["protein","kcal","neat","training","sleep","quality"];
const priorityKey = order.reduce((w,k)=>scores[k]<scores[w]?k:w, order[0]);
const labels = { kcal:"Calories", protein:"Protéines", neat:"NEAT", training:"Entraînement", sleep:"Sommeil", quality:"Qualité micro" };
// Helpers DOM
const setTxt = (id, v)=>{ const el=document.getElementById(id); if(el) el.textContent=v; };
const setFill = (id, v)=>{ const el=document.getElementById(id); if(el) el.style.width=`${clamp(v,0,100)}%`; };
// Score box
setTxt("recompScore", recompScore);
// Gauges
setFill("fill-kcal", scores.kcal); setTxt("pill-kcal", pillFromScore(scores.kcal));
setTxt("meta-kcal", `Cible ${data.targets.kcal} kcal · Réel ${data.actuals.kcal} kcal (${formatDelta(data.actuals.kcal,data.targets.kcal," kcal")})`);
setFill("fill-protein", scores.protein); setTxt("pill-protein", pillFromScore(scores.protein));
setTxt("meta-protein", `Cible ${data.targets.protein_g} g · Réel ${data.actuals.protein_g} g (${formatDelta(data.actuals.protein_g,data.targets.protein_g," g")})`);
setFill("fill-neat", scores.neat); setTxt("pill-neat", pillFromScore(scores.neat));
setTxt("meta-neat", `Cible ${data.targets.steps} pas · Réel ${data.actuals.steps} pas (${formatDelta(data.actuals.steps,data.targets.steps," pas")})`);
setFill("fill-training", scores.training); setTxt("pill-training", pillFromScore(scores.training));
setTxt("meta-training", `Cible ${data.targets.training_sessions_per_week}/sem · Réel ${data.actuals.training_sessions_per_week}/sem`);
setFill("fill-sleep", scores.sleep); setTxt("pill-sleep", pillFromScore(scores.sleep));
setTxt("meta-sleep", `Cible ${data.targets.sleep_hours} h · Réel ${data.actuals.sleep_hours} h`);
setFill("fill-quality", scores.quality); setTxt("pill-quality", pillFromScore(scores.quality));
setTxt("meta-quality", `Score micro: ${data.actuals.micro_score}/100 · Cible: ${data.targets.micro_score}/100`);
// Priorité hebdo
setTxt("prioTag", labels[priorityKey]);
const prioMain = document.getElementById("prioMain");
const prioActions = document.getElementById("prioActions");
if(prioActions) prioActions.innerHTML = "";
const addChip = (t)=>{
if(!prioActions) return;
const d=document.createElement("div"); d.className="chip"; d.textContent=t; prioActions.appendChild(d);
};
if(prioMain){
if(priorityKey==="protein"){
const need = data.targets.protein_g - data.actuals.protein_g;
prioMain.textContent = `Augmente tes protéines : +${Math.max(15, Math.round(need))} g/j pendant 7 jours.`;
addChip("1 portion protéine/jour"); addChip("25–40 g / repas"); addChip("Petit-déj + post-training");
} else if(priorityKey==="kcal"){
const delta = data.actuals.kcal - data.targets.kcal;
const dir = delta>0 ? "réduis" : "augmente";
prioMain.textContent = `Calories : ${dir} ~${Math.min(200, Math.max(80, Math.abs(Math.round(delta))))} kcal/j (sans toucher aux protéines).`;
addChip("Protéines constantes"); addChip("Ajuste G/L"); addChip("Re-test 7 jours");
} else if(priorityKey==="neat"){
const need = data.targets.steps - data.actuals.steps;
prioMain.textContent = `NEAT : +${Math.max(1500, Math.round(need))} pas/j pendant 7 jours.`;
addChip("1 marche 10–15 min"); addChip("2 mini-marches"); addChip("Régularité");
} else if(priorityKey==="sleep"){
const need = data.targets.sleep_hours - data.actuals.sleep_hours;
prioMain.textContent = `Sommeil : +${Math.max(0.5, Math.round(need*10)/10)} h au lit, 5j/7.`;
addChip("Coucher fixe"); addChip("Écran off 30 min"); addChip("Caféine plus tôt");
} else if(priorityKey==="training"){
prioMain.textContent = `Entraînement : verrouille ${data.targets.training_sessions_per_week} séances/semaine avant d’optimiser le reste.`;
addChip("Séances courtes OK"); addChip("+1 rep ou +1 kg"); addChip("Régularité");
} else {
prioMain.textContent = "Qualité micro : 2 superfoods/jour + fibres progressives, cette semaine.";
addChip("1 fruit + 1 légume"); addChip("Oméga-3 / jour"); addChip("Fermenté 3×/sem");
}
}
// Texte dynamique (simple)
const insight = document.getElementById("insightText");
if(insight){
const t = data.context || {};
const trend = t.weight_trend_7d ?? 0;
const mode = t.goal_mode || "recomp";
const phase = t.phase || "cut";
const modeTxt = {cut:"perte de gras",build:"prise de muscle",recomp:"recomposition",maintain:"maintien"}[mode] || "objectif";
let p = `<p><b>Objectif :</b> ${modeTxt}. <b>Phase :</b> ${phase}.</p>`;
if(trend < -0.1) p += `<p>Tendance poids OK (${trend} kg/sem). On réduit l’écart via <b>${labels[priorityKey]}</b>.</p>`;
else if(trend < 0.1) p += `<p>Tendance plate (${trend} kg/sem). Focus <b>${labels[priorityKey]}</b> → 7 jours → réévaluation.</p>`;
else p += `<p>Tendance en hausse (${trend} kg/sem). On sécurise l’exécution (kcal/protéines/NEAT) avant de corriger plus fort.</p>`;
insight.innerHTML = p;
}
}
📌 Ce code utilise wwLib.wwVariable.getValue(...) pour lire une variable WeWeb (c’est le pont standard entre ton app no-code et ton JS).
Et l’exécution via Workflow “Custom JavaScript action” est la voie officielle.
La meilleure pratique WeWeb (pour éviter les bugs)
HTML/CSS : dans le composant “Custom Code / HTML Embed”
JS : dans Workflow “On page load” (et éventuellement “On variable change”)
Data : dans une variable
gapDataremplie via tes Collections Supabase
C’est le trio “stable”.
Si tu veux le mode “ultra propre”
Tu peux en faire un Custom Component (Vue) réutilisable partout, mais c’est un cran plus technique. WeWeb a des ressources pour développer des composants custom si tu veux industrialiser.
Si tu me dis comment tu récupères tes “actuals” (tu logges les pas/sommeil dans tes tables, ou tu les estimes), je te fournis la VIEW SQL Supabase dashboard_gap_view qui renvoie directement l’objet complet (targets/actuals/context) — comme ça WeWeb ne fait qu’une requête et ton bloc s’alimente tout seul.