Studio: onglet generation-only (post + reel) avec galerie visuelle (download + copier texte), bouton 10 reels ; plus de publication FB ni de tache horaire
5 changed files +198 −77
modified
public/app.js
+69 −1
@@ -62,7 +62,7 @@ | |||||
| 62 | if (name === 'sessions') loadSessions(); | 62 | if (name === 'sessions') loadSessions(); | ||
| 63 | if (name === 'prompts') loadPrompts(); | 63 | if (name === 'prompts') loadPrompts(); | ||
| 64 | if (name === 'eco') { loadEco(); send({ type: 'eco_sub' }); } else { send({ type: 'eco_unsub' }); } | 64 | if (name === 'eco') { loadEco(); send({ type: 'eco_sub' }); } else { send({ type: 'eco_unsub' }); } | ||
| 65 | − if (name === 'social') loadSocial(); | 65 | + if (name === 'social' && window.__loadGallery) window.__loadGallery(); | ||
| 66 | if (name === 'settings') refreshSettingsView(); | 66 | if (name === 'settings') refreshSettingsView(); | ||
| 67 | } | 67 | } | ||
| 68 | document.querySelectorAll('.nav-btn').forEach((b) => { b.onclick = () => switchView(b.dataset.view); }); | 68 | document.querySelectorAll('.nav-btn').forEach((b) => { b.onclick = () => switchView(b.dataset.view); }); | ||
@@ -1433,3 +1433,71 @@ | |||||
| 1433 | 1433 | ||||
| 1434 | init(); | 1434 | init(); | ||
| 1435 | })(); | 1435 | })(); | ||
| 1436 | + | ||||
| 1437 | + | ||||
| 1438 | +// ===== STUDIO_SOCIAL v2 (générateurs + galerie, download + copy) ===== | ||||
| 1439 | +(function(){ | ||||
| 1440 | + const $ = (id) => document.getElementById(id); | ||||
| 1441 | + const SITE_LABEL = {"lou-ka":"Lou·Ka","immo-ka":"Immo·Ka","vrai-prix":"Vrai-Prix","auto-ka":"Auto·Ka","food-ka":"Food·Ka","fabri-ka":"Fabri·Ka","resto-ka":"Resto·Ka","sorti-ka":"Sorti·Ka","crea-ka":"Créa·Ka","job-ka":"Job·Ka","trouve-ka":"Trouve·Ka"}; | ||||
| 1442 | + async function copyText(txt, btn){ | ||||
| 1443 | + try { await navigator.clipboard.writeText(txt); } | ||||
| 1444 | + catch(e){ const t=document.createElement('textarea'); t.value=txt; document.body.appendChild(t); t.select(); try{document.execCommand('copy');}catch(_){} | ||||
| 1445 | + document.body.removeChild(t); } | ||||
| 1446 | + if(btn){ const o=btn.innerHTML; btn.innerHTML='✓ Copié'; setTimeout(()=>btn.innerHTML=o,1400); } | ||||
| 1447 | + } | ||||
| 1448 | + function esc(s){ return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); } | ||||
| 1449 | + function renderGallery(items){ | ||||
| 1450 | + const el=$('stGallery'); if(!el) return; | ||||
| 1451 | + if($('stCount')) $('stCount').textContent = items.length ? (items.length+' visuel'+(items.length>1?'s':'')) : ''; | ||||
| 1452 | + if(!items.length){ el.innerHTML='<div class="soc-sub">Aucun visuel encore — génère ton premier post ou reel ci-dessus.</div>'; return; } | ||||
| 1453 | + el.innerHTML = items.map(function(it,idx){ | ||||
| 1454 | + const media = it.kind==='reel' | ||||
| 1455 | + ? '<video src="'+it.url+'" muted loop playsinline preload="metadata" onmouseover="this.play()" onmouseout="this.pause()"></video>' | ||||
| 1456 | + : '<img src="'+it.url+'" loading="lazy">'; | ||||
| 1457 | + const badge = (SITE_LABEL[it.site]||it.site)+(it.kind==='reel'?' · Reel':' · Post'); | ||||
| 1458 | + return '<div class="st-item">' | ||||
| 1459 | + + '<div class="st-media">'+media+'<span class="st-badge">'+esc(badge)+'</span></div>' | ||||
| 1460 | + + '<div class="st-cap" id="stcap'+idx+'">'+esc(it.caption||'')+'</div>' | ||||
| 1461 | + + '<div class="st-item-actions">' | ||||
| 1462 | + + '<a class="btn-cta small" href="'+it.url+'" download="'+it.site+'-'+it.kind+(it.kind==='reel'?'.mp4':'.png')+'">⬇︎</a>' | ||||
| 1463 | + + '<button class="btn-ghost small" data-copy="'+idx+'">📋 Copier</button>' | ||||
| 1464 | + + '</div></div>'; | ||||
| 1465 | + }).join(''); | ||||
| 1466 | + el.querySelectorAll('[data-copy]').forEach(function(b){ b.onclick=function(){ copyText(items[+b.dataset.copy].caption||'', b); }; }); | ||||
| 1467 | + } | ||||
| 1468 | + async function loadGallery(){ | ||||
| 1469 | + try{ const d=await (await fetch('/api/social/gallery')).json(); | ||||
| 1470 | + renderGallery(d.items||[]); | ||||
| 1471 | + if(d.gen && d.gen.running){ if($('stGenStatus')) $('stGenStatus').textContent='⏳ génération des reels en cours…'; setTimeout(loadGallery, 12000); } | ||||
| 1472 | + else if($('stGenStatus')) $('stGenStatus').textContent=''; | ||||
| 1473 | + }catch(e){} | ||||
| 1474 | + } | ||||
| 1475 | + async function genPost(){ | ||||
| 1476 | + const g=$('stPostGen'); if(!g) return; const prev=g.textContent; g.disabled=true; g.textContent='Génération…'; | ||||
| 1477 | + try{ const r=await fetch('/api/social/generate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({site:$('stPostSite').value,prompt:$('stPostPrompt').value})}); | ||||
| 1478 | + const d=await r.json(); if(!d.draft) alert('Échec : '+(d.error||'?')); else loadGallery(); | ||||
| 1479 | + }catch(e){ alert('Échec : '+e.message); } g.disabled=false; g.textContent=prev; | ||||
| 1480 | + } | ||||
| 1481 | + async function genReel(){ | ||||
| 1482 | + const g=$('stReelGen'); if(!g) return; const prev=g.textContent; g.disabled=true; g.textContent='Génération vidéo…'; | ||||
| 1483 | + try{ const r=await fetch('/api/social/reel/generate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({site:$('stReelSite').value,prompt:$('stReelPrompt').value})}); | ||||
| 1484 | + const d=await r.json(); if(!d.draft) alert('Échec : '+(d.error||'?')); else loadGallery(); | ||||
| 1485 | + }catch(e){ alert('Échec : '+e.message); } g.disabled=false; g.textContent=prev; | ||||
| 1486 | + } | ||||
| 1487 | + async function gen10(){ | ||||
| 1488 | + const b=$('stReel10'); if(!b) return; | ||||
| 1489 | + try{ const r=await fetch('/api/social/reel/genbatch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({})}); | ||||
| 1490 | + const d=await r.json(); if($('stGenStatus')) $('stGenStatus').textContent = d.started? '⏳ 10 reels en génération…' : (d.reason||'déjà en cours'); | ||||
| 1491 | + setTimeout(loadGallery, 8000); | ||||
| 1492 | + }catch(e){ alert('Échec : '+e.message); } | ||||
| 1493 | + } | ||||
| 1494 | + function bind(){ | ||||
| 1495 | + if($('stPostGen')) $('stPostGen').onclick=genPost; | ||||
| 1496 | + if($('stReelGen')) $('stReelGen').onclick=genReel; | ||||
| 1497 | + if($('stReel10')) $('stReel10').onclick=gen10; | ||||
| 1498 | + if($('stRefresh')) $('stRefresh').onclick=loadGallery; | ||||
| 1499 | + } | ||||
| 1500 | + bind(); | ||||
| 1501 | + window.__loadGallery = loadGallery; | ||||
| 1502 | +})(); | ||||
| 1503 | + | ||||
modified
public/index.html
+20 −69
@@ -148,82 +148,34 @@ | |||||
| 148 | 148 | ||||
| 149 | <!-- ======== Vue SOCIAL ======== --> | 149 | <!-- ======== Vue SOCIAL ======== --> | ||
| 150 | <section id="view-social" class="view hidden"> | 150 | <section id="view-social" class="view hidden"> | ||
| 151 | − <div class="view-head"><h2>Social</h2></div> | 151 | + <div class="view-head"><h2>🎨 Studio</h2><button id="stRefresh" class="btn-ghost small">↻ Galerie</button></div> | ||
| 152 | + <p class="soc-sub" style="margin:-4px 0 14px">Génère des visuels prêts à publier — télécharge-les et copie le texte. Rien n'est publié automatiquement.</p> | ||||
| 152 | 153 | ||||
| 153 | − <div class="soc-card"> | 154 | + <div class="st-grid"> | ||
| 154 | − <h3>Écran du nœud (M3U96a) — pour finir un Reel</h3> | 155 | + <div class="soc-card st-gen"> | ||
| 155 | − <p class="soc-sub">Quand un Reel est prêt à publier, l'éditeur Facebook demande un clic « Suivant » manuel. Connecte-toi ici pour voir et cliquer directement dans l'app.</p> | 156 | + <h3>🖼️ Publication</h3> | ||
| 156 | − <div class="soc-actions"> | 157 | + <textarea id="stPostPrompt" rows="2" placeholder="Sujet (optionnel). Ex : dernière propriété à vendre sur Immo-Ka."></textarea> | ||
| 157 | − <button id="vncConnect" class="btn-cta small">🖥️ Connecter l'écran</button> | ||||
| 158 | − <button id="vncDisconnect" class="btn-ghost small">Déconnecter</button> | ||||
| 159 | − <span id="vncStatus" class="soc-sub"></span> | ||||
| 160 | − </div> | ||||
| 161 | − <div id="vncPanel" style="display:none;margin-top:12px;border:2px solid var(--ink,#1a1a1a);border-radius:12px;overflow:hidden;background:#0d0d0d;min-height:200px"></div> | ||||
| 162 | − </div> | ||||
| 163 | − | ||||
| 164 | − <div class="soc-card"> | ||||
| 165 | − <label class="soc-switch"> | ||||
| 166 | − <input type="checkbox" id="socAutoToggle"> | ||||
| 167 | − <span>Publication automatique sur la Page Facebook Groupe-KA</span> | ||||
| 168 | − </label> | ||||
| 169 | − <div class="soc-sub" id="socNext">—</div> | ||||
| 170 | − <div class="soc-actions"> | ||||
| 171 | − <button id="socRunNow" class="btn-cta small">⚡ Publier maintenant</button> | ||||
| 172 | − <button id="socCheckLogin" class="btn-ghost small">Vérifier Facebook</button> | ||||
| 173 | − </div> | ||||
| 174 | − <div class="soc-sub" id="socLogin"></div> | ||||
| 175 | − </div> | ||||
| 176 | − | ||||
| 177 | − <div class="soc-card"> | ||||
| 178 | − <h3>Générer un post</h3> | ||||
| 179 | − <textarea id="socPrompt" rows="3" placeholder="Décris le post voulu (optionnel). Ex : mets en valeur les aubaines de logement sous le prix du marché."></textarea> | ||||
| 180 | − <div class="soc-actions"> | ||||
| 181 | − <select id="socSite" class="soc-select"> | ||||
| 182 | − <option value="">Choix auto (fait marquant)</option> | ||||
| 183 | − <option value="lou-ka">Lou·Ka</option><option value="immo-ka">Immo·Ka</option><option value="vrai-prix">Vrai-Prix</option><option value="auto-ka">Auto·Ka</option><option value="food-ka">Food·Ka</option><option value="fabri-ka">Fabri·Ka</option><option value="resto-ka">Resto·Ka</option><option value="sorti-ka">Sorti·Ka</option><option value="crea-ka">Créa·Ka</option><option value="job-ka">Job·Ka</option><option value="trouve-ka">Trouve·Ka</option> | ||||
| 184 | − </select> | ||||
| 185 | − <button id="socGenerate" class="btn-cta small">✨ Générer</button> | ||||
| 186 | − </div> | ||||
| 187 | − <div id="socDraft" class="soc-draft hidden"> | ||||
| 188 | − <img id="socImg" alt="aperçu"> | ||||
| 189 | − <textarea id="socCaption" rows="7"></textarea> | ||||
| 190 | <div class="soc-actions"> | 158 | <div class="soc-actions"> | ||
| 191 | − <button id="socPublish" class="btn-cta small">📣 Publier ce post</button> | 159 | + <select id="stPostSite" class="soc-select"><option value="">Choix auto</option><option value="lou-ka">Lou·Ka</option><option value="immo-ka">Immo·Ka</option><option value="vrai-prix">Vrai-Prix</option><option value="auto-ka">Auto·Ka</option><option value="food-ka">Food·Ka</option><option value="fabri-ka">Fabri·Ka</option><option value="resto-ka">Resto·Ka</option><option value="sorti-ka">Sorti·Ka</option><option value="crea-ka">Créa·Ka</option><option value="job-ka">Job·Ka</option><option value="trouve-ka">Trouve·Ka</option></select> | ||
| 192 | − <button id="socRegen" class="btn-ghost small">↻ Regénérer</button> | 160 | + <button id="stPostGen" class="btn-cta small">✨ Générer une image</button> | ||
| 193 | </div> | 161 | </div> | ||
| 194 | </div> | 162 | </div> | ||
| 195 | − </div> | 163 | + <div class="soc-card st-gen"> | ||
| 196 | − | 164 | + <h3>🎬 Reel vidéo</h3> | ||
| 197 | − <div class="soc-card"> | 165 | + <textarea id="stReelPrompt" rows="2" placeholder="Sujet (optionnel). Ex : le dernier logement en annonce sur Lou-Ka."></textarea> | ||
| 198 | − <h3>Reel vidéo (animation + musique)</h3> | ||||
| 199 | − <p class="soc-sub">L'Agent KA génère un reel animé avec musique. La publication vidéo demande un clic « Suivant » manuel (limite du nœud sans écran) — utilise « Publier (assisté) » puis termine sur l'écran distant, ou télécharge pour publier depuis ton téléphone.</p> | ||||
| 200 | − <textarea id="socReelPrompt" rows="2" placeholder="Sujet du reel (optionnel)"></textarea> | ||||
| 201 | − <div class="soc-actions"> | ||||
| 202 | − <select id="socReelSite" class="soc-select"> | ||||
| 203 | − <option value="">Choix auto (fait marquant)</option> | ||||
| 204 | − <option value="lou-ka">Lou·Ka</option><option value="immo-ka">Immo·Ka</option> | ||||
| 205 | − <option value="vrai-prix">Vrai-Prix</option><option value="auto-ka">Auto·Ka</option> | ||||
| 206 | − <option value="food-ka">Food·Ka</option><option value="fabri-ka">Fabri·Ka</option> | ||||
| 207 | − <option value="resto-ka">Resto·Ka</option><option value="sorti-ka">Sorti·Ka</option> | ||||
| 208 | − <option value="crea-ka">Créa·Ka</option><option value="job-ka">Job·Ka</option> | ||||
| 209 | − <option value="trouve-ka">Trouve·Ka</option> | ||||
| 210 | − </select> | ||||
| 211 | − <button id="socReelGen" class="btn-cta small">🎬 Générer un reel</button> | ||||
| 212 | − </div> | ||||
| 213 | − <div id="socReelDraft" class="soc-draft hidden"> | ||||
| 214 | − <video id="socReelVid" controls playsinline style="width:100%;max-width:300px;border-radius:12px;border:2px solid var(--ink,#1a1a1a);align-self:center"></video> | ||||
| 215 | − <textarea id="socReelCaption" rows="6"></textarea> | ||||
| 216 | <div class="soc-actions"> | 166 | <div class="soc-actions"> | ||
| 217 | − <button id="socReelPublish" class="btn-cta small">📣 Publier (assisté)</button> | 167 | + <select id="stReelSite" class="soc-select"><option value="">Choix auto</option><option value="lou-ka">Lou·Ka</option><option value="immo-ka">Immo·Ka</option><option value="vrai-prix">Vrai-Prix</option><option value="auto-ka">Auto·Ka</option><option value="food-ka">Food·Ka</option><option value="fabri-ka">Fabri·Ka</option><option value="resto-ka">Resto·Ka</option><option value="sorti-ka">Sorti·Ka</option><option value="crea-ka">Créa·Ka</option><option value="job-ka">Job·Ka</option><option value="trouve-ka">Trouve·Ka</option></select> | ||
| 218 | − <a id="socReelDl" class="btn-ghost small" download>⬇︎ Télécharger</a> | 168 | + <button id="stReelGen" class="btn-cta small">🎬 Générer un reel</button> | ||
| 169 | + </div> | ||||
| 170 | + <div class="soc-actions" style="margin-top:8px"> | ||||
| 171 | + <button id="stReel10" class="btn-ghost small">🔥 Générer 10 reels</button> | ||||
| 172 | + <span id="stGenStatus" class="soc-sub"></span> | ||||
| 219 | </div> | 173 | </div> | ||
| 220 | </div> | 174 | </div> | ||
| 221 | </div> | 175 | </div> | ||
| 222 | 176 | ||||
| 223 | − <div class="soc-card"> | 177 | + <div class="view-head" style="margin-top:18px"><h2 style="font-size:17px">Galerie</h2><span id="stCount" class="soc-sub"></span></div> | ||
| 224 | − <h3>Journal</h3> | 178 | + <div id="stGallery" class="st-gallery"><div class="soc-sub">Aucun visuel encore — génère ton premier post ou reel ci-dessus.</div></div> | ||
| 225 | − <div id="socLog" class="soc-log"></div> | ||||
| 226 | − </div> | ||||
| 227 | </section> | 179 | </section> | ||
| 228 | 180 | ||||
| 229 | <!-- ======== Bottom nav ======== --> | 181 | <!-- ======== Bottom nav ======== --> | ||
@@ -244,7 +196,7 @@ | |||||
| 244 | <svg viewBox="0 0 24 24"><path d="M4 17l4-6 4 3 4-8 4 5" stroke="currentColor" stroke-width="1.9" fill="none" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 21h18" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg> | 196 | <svg viewBox="0 0 24 24"><path d="M4 17l4-6 4 3 4-8 4 5" stroke="currentColor" stroke-width="1.9" fill="none" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 21h18" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg> | ||
| 245 | <span>Écosystème</span><i class="badge hidden"></i> | 197 | <span>Écosystème</span><i class="badge hidden"></i> | ||
| 246 | </button> | 198 | </button> | ||
| 247 | − <!-- social-hidden (désactivé temporairement) --> | 199 | + <button data-view="social" class="nav-btn"><svg viewBox="0 0 24 24"><path d="M4 5h16v11H4z M8 20h8" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/><path d="M10 8l5 2.5-5 2.5z" fill="currentColor"/></svg><span>Studio</span><i class="badge hidden"></i></button> | ||
| 248 | <button data-view="settings" class="nav-btn"> | 200 | <button data-view="settings" class="nav-btn"> | ||
| 249 | <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3.2" stroke="currentColor" stroke-width="1.8" fill="none"/><path d="M12 2.8v3M12 18.2v3M2.8 12h3M18.2 12h3M5.5 5.5l2.1 2.1M16.4 16.4l2.1 2.1M18.5 5.5l-2.1 2.1M7.6 16.4l-2.1 2.1" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg> | 201 | <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3.2" stroke="currentColor" stroke-width="1.8" fill="none"/><path d="M12 2.8v3M12 18.2v3M2.8 12h3M18.2 12h3M5.5 5.5l2.1 2.1M16.4 16.4l2.1 2.1M18.5 5.5l-2.1 2.1M7.6 16.4l-2.1 2.1" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg> | ||
| 250 | <span>Réglages</span><i class="badge hidden"></i> | 202 | <span>Réglages</span><i class="badge hidden"></i> | ||
@@ -264,6 +216,5 @@ | |||||
| 264 | <script src="/vendor/marked.min.js"></script> | 216 | <script src="/vendor/marked.min.js"></script> | ||
| 265 | <script src="/vendor/highlight.min.js"></script> | 217 | <script src="/vendor/highlight.min.js"></script> | ||
| 266 | <script src="/app.js"></script> | 218 | <script src="/app.js"></script> | ||
| 267 | −<script type="module">import RFB from "/vendor/novnc/core/rfb.js?v=3"; window.KA_RFB=RFB;</script> | ||||
| 268 | </body> | 219 | </body> | ||
| 269 | </html> | 220 | </html> | ||
modified
public/style.css
+14 −0
@@ -663,3 +663,17 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 663 | 663 | ||||
| 664 | #vncPanel canvas{max-width:100%;height:auto;display:block;margin:0 auto} | 664 | #vncPanel canvas{max-width:100%;height:auto;display:block;margin:0 auto} | ||
| 665 | #vncPanel{max-height:70vh} | 665 | #vncPanel{max-height:70vh} | ||
| 666 | + | ||||
| 667 | +/* ---- Studio ---- */ | ||||
| 668 | +.st-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px} | ||||
| 669 | +@media(max-width:640px){.st-grid{grid-template-columns:1fr}} | ||||
| 670 | +.st-gen h3{margin:0 0 8px} | ||||
| 671 | +.st-gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:14px;margin-top:8px} | ||||
| 672 | +.st-item{background:var(--card,#fff);border:2px solid var(--ink,#1a1a1a);border-radius:14px;overflow:hidden;box-shadow:3px 3px 0 var(--ink,#1a1a1a);display:flex;flex-direction:column} | ||||
| 673 | +.st-media{position:relative;aspect-ratio:4/5;background:#0d0d0d;overflow:hidden} | ||||
| 674 | +.st-media img,.st-media video{width:100%;height:100%;object-fit:cover;display:block} | ||||
| 675 | +.st-badge{position:absolute;left:8px;top:8px;background:rgba(13,13,13,.82);color:#fff;font-size:11px;font-weight:800;padding:4px 9px;border-radius:999px;letter-spacing:.02em} | ||||
| 676 | +.st-cap{font-size:11px;line-height:1.35;color:var(--muted,#555);padding:8px 10px;max-height:76px;overflow:auto;white-space:pre-wrap} | ||||
| 677 | +.st-item-actions{display:flex;gap:6px;padding:0 10px 10px;margin-top:auto} | ||||
| 678 | +.st-item-actions .btn-cta.small{flex:0 0 auto;text-decoration:none;text-align:center} | ||||
| 679 | +.st-item-actions .btn-ghost.small{flex:1} | ||||
modified
server/server.js
+17 −1
@@ -11,7 +11,7 @@ import { spawn } from 'node:child_process'; | |||||
| 11 | import { fileURLToPath } from 'node:url'; | 11 | import { fileURLToPath } from 'node:url'; | ||
| 12 | import { WebSocketServer } from 'ws'; | 12 | import { WebSocketServer } from 'ws'; | ||
| 13 | import { startMonitor, ecoSummary, ecoSite, ecoIncidents, ecoNodes, getIcon, SITES } from './monitor.js'; | 13 | import { startMonitor, ecoSummary, ecoSite, ecoIncidents, ecoNodes, getIcon, SITES } from './monitor.js'; | ||
| 14 | −import { initSocial, socialState, socialLog, setAuto, generateDraft, publishDraft, runAutoCycle, cardPath, fbLoginState, generateReelDraft, publishReelDraft, runReelCycle, reelPath } from './social.js'; | 14 | +import { initSocial, socialState, socialLog, setAuto, generateDraft, publishDraft, runAutoCycle, cardPath, fbLoginState, generateReelDraft, publishReelDraft, runReelCycle, reelPath, startReelBatch, batchStatus, REEL_BATCH_DEFAULT, getGallery, startGenBatch, genBatchStatus } from './social.js'; | ||
| 15 | import { execFile } from 'node:child_process'; | 15 | import { execFile } from 'node:child_process'; | ||
| 16 | 16 | ||||
| 17 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); | 17 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); | ||
@@ -727,6 +727,22 @@ const server = http.createServer(async (req, res) => { | |||||
| 727 | const r = await runReelCycle('reel-manuel'); | 727 | const r = await runReelCycle('reel-manuel'); | ||
| 728 | return json(res, r.ok ? 200 : 502, r); | 728 | return json(res, r.ok ? 200 : 502, r); | ||
| 729 | } | 729 | } | ||
| 730 | + if (p === '/api/social/gallery' && req.method === 'GET') return json(res, 200, { items: getGallery(), gen: genBatchStatus() }); | ||||
| 731 | + if (p === '/api/social/reel/genbatch' && req.method === 'POST') { | ||||
| 732 | + let body; try { body = JSON.parse(await readBody(req)); } catch { body = {}; } | ||||
| 733 | + const sites = (Array.isArray(body.sites) && body.sites.length) ? body.sites : REEL_BATCH_DEFAULT; | ||||
| 734 | + audit({ kind: 'social_genbatch', n: sites.length, ip: clientIp(req) }); | ||||
| 735 | + const r = startGenBatch(sites); | ||||
| 736 | + return json(res, r.started ? 200 : 409, r); | ||||
| 737 | + } | ||||
| 738 | + if (p === '/api/social/reel/batch' && req.method === 'POST') { | ||||
| 739 | + let body; try { body = JSON.parse(await readBody(req)); } catch { body = {}; } | ||||
| 740 | + const sites = (Array.isArray(body.sites) && body.sites.length) ? body.sites : REEL_BATCH_DEFAULT; | ||||
| 741 | + audit({ kind: 'social_reel_batch', n: sites.length, ip: clientIp(req) }); | ||||
| 742 | + const r = startReelBatch(sites); | ||||
| 743 | + return json(res, r.started ? 200 : 409, r); | ||||
| 744 | + } | ||||
| 745 | + if (p === '/api/social/reel/batchstatus' && req.method === 'GET') return json(res, 200, batchStatus()); | ||||
| 730 | if (p.startsWith('/api/social/reel/') && req.method === 'GET') { | 746 | if (p.startsWith('/api/social/reel/') && req.method === 'GET') { | ||
| 731 | const fp = reelPath(p.split('/')[4] || ''); | 747 | const fp = reelPath(p.split('/')[4] || ''); | ||
| 732 | if (!fp) { res.writeHead(404); return res.end(); } | 748 | if (!fp) { res.writeHead(404); return res.end(); } | ||
modified
server/social.js
+78 −6
@@ -201,8 +201,9 @@ async function _fbLoginState() { | |||||
| 201 | 201 | ||||
| 202 | // ---------- publication (texte + 1 image) sur la Page ---------- | 202 | // ---------- publication (texte + 1 image) sur la Page ---------- | ||
| 203 | export function publishToPage(caption, imagePath) { return withSafari(() => _publishMedia(caption, imagePath, false)); } | 203 | export function publishToPage(caption, imagePath) { return withSafari(() => _publishMedia(caption, imagePath, false)); } | ||
| 204 | −export function publishReelToPage(caption, videoPath) { return withSafari(() => _publishMedia(caption, videoPath, true)); } | 204 | +export function publishReelToPage(caption, videoPath) { return withSafari(() => _publishMedia(caption, videoPath, true, false)); } | ||
| 205 | −async function _publishMedia(caption, mediaPath, isVideo) { | 205 | +export function publishReelAssisted(caption, videoPath) { return withSafari(() => _publishMedia(caption, videoPath, true, true)); } | ||
| 206 | +async function _publishMedia(caption, mediaPath, isVideo, assisted) { | ||||
| 206 | await gotoPage(); | 207 | await gotoPage(); | ||
| 207 | await sleep(9000); | 208 | await sleep(9000); | ||
| 208 | 209 | ||||
@@ -373,7 +374,10 @@ async function _publishMedia(caption, mediaPath, isVideo) { | |||||
| 373 | return 'clicked'; | 374 | return 'clicked'; | ||
| 374 | })();`; | 375 | })();`; | ||
| 375 | const canPublishJS = `(function(){var ok=false;document.querySelectorAll('div[role=dialog] [role=button]').forEach(function(b){var al=(b.getAttribute('aria-label')||b.innerText||'').trim();if((al==='Publier'||al==='Partager')&&b.getAttribute('aria-disabled')!=='true')ok=true;});return ok?'YES':'NO';})();`; | 376 | const canPublishJS = `(function(){var ok=false;document.querySelectorAll('div[role=dialog] [role=button]').forEach(function(b){var al=(b.getAttribute('aria-label')||b.innerText||'').trim();if((al==='Publier'||al==='Partager')&&b.getAttribute('aria-disabled')!=='true')ok=true;});return ok?'YES':'NO';})();`; | ||
| 376 | − if (isVideo) { | 377 | + if (isVideo && assisted) { | ||
| 378 | + // mode assisté : l'utilisateur clique « Suivant »/« Publier » sur l'écran du nœud | ||||
| 379 | + logEvent({ kind: 'reel_waiting_user' }); | ||||
| 380 | + } else if (isVideo) { | ||||
| 377 | // Reel/vidéo : avancer les « Suivant » par focus + touche Entrée réelle | 381 | // Reel/vidéo : avancer les « Suivant » par focus + touche Entrée réelle | ||
| 378 | for (let i = 0; i < 6; i++) { | 382 | for (let i = 0; i < 6; i++) { | ||
| 379 | if ((await osaJS(canPublishJS)) === 'YES') break; | 383 | if ((await osaJS(canPublishJS)) === 'YES') break; | ||
@@ -408,7 +412,7 @@ async function _publishMedia(caption, mediaPath, isVideo) { | |||||
| 408 | return open ? 'OPEN' : 'GONE'; | 412 | return open ? 'OPEN' : 'GONE'; | ||
| 409 | })();`; | 413 | })();`; | ||
| 410 | let gone = 'OPEN'; | 414 | let gone = 'OPEN'; | ||
| 411 | − const goneTries = isVideo ? 40 : 18; | 415 | + const goneTries = assisted ? 45 : (isVideo ? 40 : 18); | ||
| 412 | for (let i = 0; i < goneTries; i++) { | 416 | for (let i = 0; i < goneTries; i++) { | ||
| 413 | await sleep(8000); | 417 | await sleep(8000); | ||
| 414 | await dismissModals(2); // ferme toute boîte non bloquante en route | 418 | await dismissModals(2); // ferme toute boîte non bloquante en route | ||
@@ -580,7 +584,9 @@ export async function generateDraft({ site = '', prompt = '' } = {}) { | |||||
| 580 | } | 584 | } | ||
| 581 | const image = await renderCard(insight); | 585 | const image = await renderCard(insight); | ||
| 582 | const caption = await writeCaption(insight, prompt); | 586 | const caption = await writeCaption(insight, prompt); | ||
| 583 | − return { insight, image, caption, imageUrl: '/api/social/card/' + path.basename(image) }; | 587 | + const imageUrl='/api/social/card/'+path.basename(image); | ||
| 588 | + addToGallery({ kind:'post', site:insight.site, genre:insight.genre, caption, file:path.basename(image), url:imageUrl, ts:Date.now() }); | ||||
| 589 | + return { insight, image, caption, imageUrl }; | ||||
| 584 | } | 590 | } | ||
| 585 | 591 | ||||
| 586 | // cycle complet automatique : choisir -> rendre -> rédiger -> publier | 592 | // cycle complet automatique : choisir -> rendre -> rédiger -> publier | ||
@@ -653,7 +659,9 @@ export async function generateReelDraft({ site = '', prompt = '' } = {}) { | |||||
| 653 | } | 659 | } | ||
| 654 | const video = await makeReel(insight); | 660 | const video = await makeReel(insight); | ||
| 655 | const caption = await writeCaption(insight, prompt); | 661 | const caption = await writeCaption(insight, prompt); | ||
| 656 | − return { insight, video, caption, videoUrl: '/api/social/reel/' + path.basename(video) }; | 662 | + const videoUrl='/api/social/reel/'+path.basename(video); | ||
| 663 | + addToGallery({ kind:'reel', site:insight.site, genre:insight.genre, caption, file:path.basename(video), url:videoUrl, ts:Date.now() }); | ||||
| 664 | + return { insight, video, caption, videoUrl }; | ||||
| 657 | } | 665 | } | ||
| 658 | 666 | ||||
| 659 | export async function publishReelDraft({ caption, video }) { | 667 | export async function publishReelDraft({ caption, video }) { | ||
@@ -686,6 +694,70 @@ export async function runReelCycle(trigger = 'reel') { | |||||
| 686 | } | 694 | } | ||
| 687 | } | 695 | } | ||
| 688 | 696 | ||||
| 697 | +// ---------- Galerie (tout le contenu généré : posts + reels) ---------- | ||||
| 698 | +const GALLERY_PATH = path.join(SOCIAL_DIR, 'gallery.json'); | ||||
| 699 | +function loadGallery(){ try { return JSON.parse(fs.readFileSync(GALLERY_PATH,'utf8')); } catch { return []; } } | ||||
| 700 | +function saveGallery(g){ try { fs.writeFileSync(GALLERY_PATH, JSON.stringify(g.slice(0,200))); } catch {} } | ||||
| 701 | +function addToGallery(item){ const g=loadGallery(); g.unshift(item); saveGallery(g); } | ||||
| 702 | +export function getGallery(){ return loadGallery().slice(0,60); } | ||||
| 703 | + | ||||
| 704 | +let genBatchRunning = false; | ||||
| 705 | +export function startGenBatch(sites){ | ||||
| 706 | + if (genBatchRunning) return { started:false, reason:'génération en cours' }; | ||||
| 707 | + genBatchRunning = true; | ||||
| 708 | + (async () => { | ||||
| 709 | + logEvent({ kind:'genbatch_start', total:sites.length }); | ||||
| 710 | + for (let i=0;i<sites.length;i++){ | ||||
| 711 | + const site=sites[i]; | ||||
| 712 | + try{ | ||||
| 713 | + logEvent({ kind:'genbatch_item', i:i+1, total:sites.length, site }); | ||||
| 714 | + const insight=await pickInsight({ site }); | ||||
| 715 | + await decorateForPrompt(insight); | ||||
| 716 | + const video=await makeReel(insight); | ||||
| 717 | + const caption=await writeCaption(insight); | ||||
| 718 | + addToGallery({ kind:'reel', site, genre:insight.genre, caption, file:path.basename(video), | ||||
| 719 | + url:'/api/social/reel/'+path.basename(video), ts:Date.now() }); | ||||
| 720 | + logEvent({ kind:'genbatch_done_item', i:i+1, site }); | ||||
| 721 | + }catch(e){ logEvent({ kind:'genbatch_error', i:i+1, site, error:e.message }); } | ||||
| 722 | + } | ||||
| 723 | + logEvent({ kind:'genbatch_done', total:sites.length }); | ||||
| 724 | + genBatchRunning=false; | ||||
| 725 | + })(); | ||||
| 726 | + return { started:true, count:sites.length }; | ||||
| 727 | +} | ||||
| 728 | +export function genBatchStatus(){ return { running: genBatchRunning }; } | ||||
| 729 | + | ||||
| 730 | +// ---------- Lot de reels (génération + publication assistée) ---------- | ||||
| 731 | +let batchRunning = false; | ||||
| 732 | +export const REEL_BATCH_DEFAULT = ['lou-ka','immo-ka','fabri-ka','sorti-ka','food-ka','auto-ka','vrai-prix','crea-ka','trouve-ka','resto-ka']; | ||||
| 733 | +export function startReelBatch(sites) { | ||||
| 734 | + if (batchRunning) return { started: false, reason: 'lot déjà en cours' }; | ||||
| 735 | + batchRunning = true; | ||||
| 736 | + (async () => { | ||||
| 737 | + logEvent({ kind: 'batch_start', total: sites.length }); | ||||
| 738 | + for (let i = 0; i < sites.length; i++) { | ||||
| 739 | + const site = sites[i]; | ||||
| 740 | + try { | ||||
| 741 | + logEvent({ kind: 'batch_item', i: i + 1, total: sites.length, site, step: 'génération vidéo' }); | ||||
| 742 | + const insight = await pickInsight({ site }); | ||||
| 743 | + await decorateForPrompt(insight); | ||||
| 744 | + const video = await makeReel(insight); | ||||
| 745 | + const caption = await writeCaption(insight); | ||||
| 746 | + logEvent({ kind: 'batch_ready', i: i + 1, total: sites.length, site, genre: insight.genre, step: '⏳ CLIQUE « Suivant » puis « Publier » sur l\'écran du nœud' }); | ||||
| 747 | + await publishReelAssisted(caption, video); | ||||
| 748 | + state.lastPostAt = Date.now(); state.postCount = (state.postCount || 0) + 1; saveState(); | ||||
| 749 | + logEvent({ kind: 'published', trigger: 'batch', format: 'reel', site, headline: insight.headline, caption, video: video.split('/').pop() }); | ||||
| 750 | + } catch (e) { | ||||
| 751 | + logEvent({ kind: 'batch_error', i: i + 1, site, error: e.message }); | ||||
| 752 | + } | ||||
| 753 | + } | ||||
| 754 | + logEvent({ kind: 'batch_done', total: sites.length }); | ||||
| 755 | + batchRunning = false; | ||||
| 756 | + })(); | ||||
| 757 | + return { started: true, count: sites.length }; | ||||
| 758 | +} | ||||
| 759 | +export function batchStatus() { return { running: batchRunning }; } | ||||
| 760 | + | ||||
| 689 | // ---------- planificateur horaire ---------- | 761 | // ---------- planificateur horaire ---------- | ||
| 690 | function scheduleNext() { | 762 | function scheduleNext() { | ||
| 691 | clearTimeout(timer); | 763 | clearTimeout(timer); | ||
| 692 | 764 | ||||