SPB Git

spb/admin-ka

Public

Studio: onglet generation-only (post + reel) avec galerie visuelle (download + copier texte), bouton 10 reels ; plus de publication FB ni de tache horaire

Simon-Pierre Boucher committed (Aug 23, 2026) parent 6e04c50 Browse files

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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
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
Menu

Theme