225 lines
12 KiB
HTML
225 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr"><head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>LinkedIn Control V97 - WEVAL</title>
|
|
<style>
|
|
:root{--bg:#0a0e1a;--bg2:#141b2d;--bg3:#1e2740;--fg:#e8ecf4;--t2:#9aa5c0;--t3:#5f6b85;--brd:rgba(255,255,255,.08);--gold:#d4af37;--em:#10b981;--cy:#06b6d4;--am:#f59e0b;--co:#ef4444;--vi:#8b5cf6;--sa:#3b82f6;}
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{background:var(--bg);color:var(--fg);font-family:'SF Pro Display',system-ui,sans-serif;min-height:100vh;padding:20px}
|
|
.hdr{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:20px;padding-bottom:16px;border-bottom:1px solid var(--brd)}
|
|
.hdr h1{font-size:24px;font-weight:700}
|
|
.hdr h1 span{color:var(--gold)}
|
|
.sub{color:var(--t2);font-size:12px;margin-top:4px}
|
|
.api-badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:10px;margin-left:8px}
|
|
.api-badge.ready{background:var(--em);color:#000}
|
|
.api-badge.missing{background:var(--co);color:#fff}
|
|
.btns{display:flex;gap:8px;flex-wrap:wrap}
|
|
.btn{padding:8px 14px;background:var(--bg3);border:1px solid var(--brd);border-radius:6px;color:var(--fg);cursor:pointer;font-size:12px;font-weight:500;transition:all .15s;text-decoration:none;display:inline-block}
|
|
.btn:hover{background:var(--gold);color:#000;border-color:var(--gold)}
|
|
.btn.primary{background:var(--gold);color:#000}
|
|
.btn.danger{background:var(--co);color:#fff}
|
|
.btn.success{background:var(--em);color:#000}
|
|
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px;margin-bottom:20px}
|
|
.mc{background:var(--bg2);border:1px solid var(--brd);border-left:3px solid var(--gold);padding:12px 14px;border-radius:8px;cursor:pointer;transition:all .15s}
|
|
.mc:hover{transform:translateY(-2px);border-left-color:var(--gold);box-shadow:0 4px 12px rgba(212,175,55,.2)}
|
|
.mc.v{border-left-color:var(--vi)}.mc.c{border-left-color:var(--cy)}.mc.e{border-left-color:var(--em)}.mc.a{border-left-color:var(--am)}.mc.s{border-left-color:var(--sa)}.mc.r{border-left-color:var(--co)}
|
|
.mc-l{font-size:9px;color:var(--t2);text-transform:uppercase;letter-spacing:1px}
|
|
.mc-v{font-size:26px;font-weight:700;margin:4px 0}
|
|
.mc-s{font-size:10px;color:var(--em)}
|
|
.cols{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;margin-bottom:24px}
|
|
@media(max-width:1200px){.cols{grid-template-columns:1fr}}
|
|
.col{background:var(--bg2);border:1px solid var(--brd);border-radius:10px;padding:14px;max-height:700px;overflow-y:auto}
|
|
.col h2{font-size:12px;font-weight:700;color:var(--t2);text-transform:uppercase;letter-spacing:1.5px;margin-bottom:12px;display:flex;align-items:center;justify-content:space-between}
|
|
.col h2 .count{background:var(--bg3);padding:2px 8px;border-radius:10px;color:var(--gold);font-size:11px}
|
|
.post{background:var(--bg3);border:1px solid var(--brd);border-radius:8px;padding:12px;margin-bottom:10px}
|
|
.post-h{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;font-size:10px;color:var(--t3)}
|
|
.post-h .theme{color:var(--cy);font-weight:600;text-transform:uppercase}
|
|
.post-body{font-size:11px;color:var(--fg);line-height:1.5;white-space:pre-wrap;max-height:180px;overflow-y:auto;background:var(--bg);padding:8px;border-radius:4px;margin-bottom:8px}
|
|
.post-meta{display:flex;gap:8px;font-size:9px;color:var(--t3);margin-bottom:8px;flex-wrap:wrap}
|
|
.post-actions{display:flex;gap:4px;flex-wrap:wrap}
|
|
.post-actions button{padding:4px 9px;background:var(--bg);border:1px solid var(--brd);border-radius:4px;color:var(--t2);font-size:10px;cursor:pointer}
|
|
.post-actions button:hover{background:var(--gold);color:#000;border-color:var(--gold)}
|
|
.post-actions button.approve{background:var(--em);color:#fff;border-color:var(--em)}
|
|
.post-actions button.publish{background:var(--sa);color:#fff;border-color:var(--sa)}
|
|
.post-actions button.reject{background:var(--co);color:#fff;border-color:var(--co)}
|
|
.log-panel{background:var(--bg2);border:1px solid var(--brd);border-radius:8px;padding:12px;margin-top:20px}
|
|
.log-panel pre{font-size:10px;color:var(--t2);max-height:150px;overflow-y:auto;background:var(--bg);padding:8px;border-radius:4px;white-space:pre-wrap}
|
|
.spnr{display:inline-block;width:12px;height:12px;border:2px solid var(--gold);border-top:2px solid transparent;border-radius:50%;animation:sp 1s linear infinite;vertical-align:middle}
|
|
@keyframes sp{to{transform:rotate(360deg)}}
|
|
.empty{text-align:center;color:var(--t3);padding:30px;font-size:12px;font-style:italic}
|
|
select{background:var(--bg3);border:1px solid var(--brd);border-radius:4px;color:var(--fg);padding:4px 8px;font-size:11px}
|
|
</style></head><body>
|
|
|
|
<div class="hdr">
|
|
<div>
|
|
<h1>LinkedIn <span>Control Center</span> <span style="font-size:13px;color:var(--t3)">V97</span> <span id="apiBadge" class="api-badge missing">API: loading</span></h1>
|
|
<div class="sub">Full automation · approve → schedule → auto-publish · cron */15min · sovereign Ollama</div>
|
|
</div>
|
|
<div class="btns">
|
|
<a href="/linkedin-automation-v96.html" class="btn">← V96 Generator</a>
|
|
<a href="/weval-technology-platform.html" class="btn">WTP</a>
|
|
<a href="/wevia-master.html" class="btn primary">WEVIA Chat</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid" id="kpiGrid"></div>
|
|
|
|
<div class="btns" style="margin-bottom:20px">
|
|
<button class="btn primary" onclick="gen('wevia_sovereign_ai')">🤖 Generate WEVIA AI</button>
|
|
<button class="btn primary" onclick="gen('ethica_hcp')">💊 Generate Ethica</button>
|
|
<button class="btn primary" onclick="gen('vistex_sap')">🏢 Vistex SAP</button>
|
|
<button class="btn primary" onclick="gen('case_study')">📊 Case Study</button>
|
|
<button class="btn" onclick="refreshAll()">🔄 Refresh</button>
|
|
<button class="btn success" onclick="triggerCron()">⚡ Trigger Cron Now</button>
|
|
</div>
|
|
|
|
<div class="cols">
|
|
<div class="col">
|
|
<h2>📝 Drafts <span id="draftsCount" class="count">0</span></h2>
|
|
<div id="drafts"></div>
|
|
</div>
|
|
<div class="col">
|
|
<h2>⏰ Scheduled <span id="schedCount" class="count">0</span></h2>
|
|
<div id="scheduled"></div>
|
|
</div>
|
|
<div class="col">
|
|
<h2>✅ Published <span id="pubCount" class="count">0</span></h2>
|
|
<div id="published"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="log-panel">
|
|
<h2 style="font-size:11px;color:var(--t2);margin-bottom:8px">Cron Log (last 50)</h2>
|
|
<pre id="log">Loading...</pre>
|
|
</div>
|
|
|
|
<script>
|
|
const API='/api/v97-linkedin-control.php';
|
|
const V96API='/api/v96-linkedin-automation.php';
|
|
|
|
async function loadAll(){
|
|
await loadOverview();
|
|
await loadQueues();
|
|
await loadLog();
|
|
}
|
|
window.refreshAll=loadAll;
|
|
|
|
async function loadOverview(){
|
|
const r=await fetch(API+'?action=overview');
|
|
const d=await r.json();
|
|
document.getElementById('apiBadge').textContent='API: '+(d.linkedin_api_ready?'Ready':'Missing token');
|
|
document.getElementById('apiBadge').className='api-badge '+(d.linkedin_api_ready?'ready':'missing');
|
|
const st=d.page_stats||{};
|
|
document.getElementById('kpiGrid').innerHTML=[
|
|
['Drafts','v',d.queue_drafts,'waiting approval'],
|
|
['Approved','c',d.approved,'ready to schedule'],
|
|
['Scheduled','a',d.scheduled,'auto-publish'],
|
|
['Published Today','e',d.published_today,'total: '+d.published_count],
|
|
['Followers','s',st.followers||0,'+'+(st.last_7d_new_followers||0)+' 7d'],
|
|
['Impressions 7d','e',st.last_7d_post_impressions||0,'+'+(st.pct_growth_impressions||0)+'%'],
|
|
['Pixel Hits','c',d.pixel_hits_month||0,'LinkedIn ref'],
|
|
].map(([l,c,v,sub])=>`<div class="mc ${c}"><div class="mc-l">${l}</div><div class="mc-v">${v}</div><div class="mc-s">${sub}</div></div>`).join('');
|
|
}
|
|
|
|
async function loadQueues(){
|
|
const r=await fetch(API+'?action=all_queues');
|
|
const d=await r.json();
|
|
// Drafts
|
|
const drafts=(d.queue_drafts||[]).reverse();
|
|
document.getElementById('draftsCount').textContent=drafts.length;
|
|
document.getElementById('drafts').innerHTML=drafts.length?drafts.map(p=>renderPost(p,'draft')).join(''):'<div class="empty">No drafts</div>';
|
|
// Scheduled
|
|
const sched=d.scheduled||[];
|
|
document.getElementById('schedCount').textContent=sched.length;
|
|
document.getElementById('scheduled').innerHTML=sched.length?sched.map(p=>renderPost(p,'scheduled')).join(''):'<div class="empty">Nothing scheduled</div>';
|
|
// Published
|
|
const pub=(d.published||[]).slice().reverse().slice(0,10);
|
|
document.getElementById('pubCount').textContent=(d.published||[]).length;
|
|
document.getElementById('published').innerHTML=pub.length?pub.map(p=>renderPost(p,'published')).join(''):'<div class="empty">No published yet</div>';
|
|
}
|
|
|
|
function renderPost(p,col){
|
|
const body=(p.post||'').replace(/</g,'<');
|
|
const dt=p.published_at||p.scheduled_at||p.approved_at||p.ts||'';
|
|
const status=p.status||'draft_queued';
|
|
let actions='';
|
|
if(col==='draft'){
|
|
if(status==='due_pending_manual'){
|
|
actions=`<button class="publish" onclick="act('publish_now','${p.id}')">🚀 Mark Published</button><button class="reject" onclick="act('reject','${p.id}')">🗑 Delete</button>`;
|
|
}else{
|
|
actions=`
|
|
<button class="approve" onclick="act('approve','${p.id}')">✓ Approve</button>
|
|
<button onclick="schedulePrompt('${p.id}')">⏰ Schedule</button>
|
|
<button class="publish" onclick="act('publish_now','${p.id}')">🚀 Publish Now</button>
|
|
<button onclick="copyPost('${p.id}')">📋 Copy</button>
|
|
<button class="reject" onclick="act('reject','${p.id}')">🗑</button>
|
|
`;
|
|
}
|
|
}else if(col==='scheduled'){
|
|
actions=`<span style="color:var(--am);font-size:10px">⏰ ${new Date(dt).toLocaleString('fr')}</span>`;
|
|
}else{
|
|
actions=`<span style="color:var(--em);font-size:10px">✅ ${new Date(dt).toLocaleString('fr')} via ${p.published_via||'?'}</span>`;
|
|
}
|
|
return `<div class="post" id="p-${p.id}">
|
|
<div class="post-h"><span class="theme">${p.theme||'?'}</span><span>${(p.chars||0)}c · ${p.metrics_count||0}m · ${p.hashtags_count||0}#</span></div>
|
|
<div class="post-body">${body}</div>
|
|
<div class="post-meta"><span>${(p.latency_ms||0)}ms</span><span>💰 0€</span><span>${p.provider||''}</span></div>
|
|
<div class="post-actions">${actions}</div>
|
|
</div>`;
|
|
}
|
|
|
|
async function act(action,id){
|
|
if(action==='reject' && !confirm('Delete this post?'))return;
|
|
const r=await fetch(API+'?action='+action+'&id='+id);
|
|
const d=await r.json();
|
|
if(d.ok){loadAll();}else{alert('Failed: '+JSON.stringify(d));}
|
|
}
|
|
|
|
async function schedulePrompt(id){
|
|
const hrs=prompt('Schedule in how many hours from now?','2');
|
|
if(!hrs)return;
|
|
const when=new Date(Date.now()+parseInt(hrs)*3600*1000).toISOString();
|
|
const r=await fetch(API+'?action=schedule&id='+id+'&when='+encodeURIComponent(when));
|
|
const d=await r.json();
|
|
if(d.ok){loadAll();}else{alert('Failed: '+JSON.stringify(d));}
|
|
}
|
|
|
|
async function copyPost(id){
|
|
const q=document.getElementById('p-'+id);
|
|
const txt=q?.querySelector('.post-body')?.textContent||'';
|
|
navigator.clipboard.writeText(txt).then(()=>{alert('Copied! Open LinkedIn + paste');window.open('https://www.linkedin.com/company/69533182/admin/page-posts/published/','_blank');});
|
|
}
|
|
|
|
async function gen(theme){
|
|
const b=event.target;
|
|
const orig=b.innerHTML;
|
|
b.innerHTML='<span class="spnr"></span> Generating...';
|
|
b.disabled=true;
|
|
try{
|
|
await fetch(V96API+'?action=generate_post&theme='+theme);
|
|
await loadAll();
|
|
}catch(e){alert(e.message);}
|
|
b.innerHTML=orig;b.disabled=false;
|
|
}
|
|
|
|
async function triggerCron(){
|
|
const r=await fetch(API+'?action=auto_publish_due');
|
|
const d=await r.json();
|
|
alert('Cron triggered: published='+d.published+' failed='+d.failed+' remaining='+d.remaining_scheduled);
|
|
loadAll();
|
|
}
|
|
|
|
async function loadLog(){
|
|
try{
|
|
const r=await fetch(API+'?action=log');
|
|
const d=await r.json();
|
|
document.getElementById('log').textContent=d.log||'(empty - no cron runs yet)';
|
|
}catch(e){}
|
|
}
|
|
|
|
loadAll();
|
|
setInterval(loadAll,30000);
|
|
</script>
|
|
</body></html>
|