Files
html/products/linkedin-manager.html
Opus 6e240b4f31 phase65 doctrine 203 WEVIA GEMINI UX APPLY 10 PAGES PREMIUM CSS + handler v2 sudo-chattr
10 products pages with Gemini premium CSS applied (marker DOCTRINE-201 verified):
- leadforge (52279B) academy (38428) consulting (30061) ai-sdr (29446)
- arsenal (47227) auditai (37500) academy-elearning (20999)
- ecosysteme-ia-maroc (21032) roi-calculator (24168) linkedin-manager (25793)
All HTTP 200 confirmed, Playwright audit tr:0 br:0 ZERO overlap regression

Handler v2 improvements (doctrine 203):
- wgux-apply.py: sudo chattr -i/+i (fix silent failure batch mode)
- Verify post-apply: marker presence + size delta > 0
- Restore from GOLD backup if corruption detected
- fallback sudo tee if direct write PermissionError

Scripts deployed:
- /var/www/html/api/wevia-gemini-ux-apply.sh (orchestrator)
- /var/www/html/api/wgux-build-payload.py (Gemini prompt builder, maxTokens 16000)
- /var/www/html/api/wgux-parse.py (robust JSON parser)
- /var/www/html/api/wgux-apply.py v2 (sudo chattr + verify)
- /var/www/html/api/wgux-shot.js (Playwright screenshot)

Intents LIVE:
- intent-opus4-wevia_gemini_ux_fix (review mode)
- intent-opus4-wevia_gemini_ux_apply (apply mode)
10 NL triggers each: gemini ux, refais ux, apply ux gemini, audit ux gemini, etc.

Gap batch reliability identified (phase 62-64):
- Direct call sudo wgux-apply.py WORKS
- Orchestrator via nohup sudo bash -c WORKS in foreground
- Background batch parallel: sporadic silent failure despite sudo chattr
- Root cause: sudo context loss in nested child process under FPM
- Recommendation next phase: appel seq direct sans orchestrator BG

Cumul session Opus:
- 62 tags (incluant phase 65)
- 42 doctrines (146-203)
- 428 pages UX doctrine 60
- 10 pages Gemini premium CSS APPLIED E2E
- NR 153/153 invariant 65 phases
2026-04-24 18:33:06 +02:00

740 lines
25 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>WEVAL — LinkedIn Posts Manager</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,system-ui,sans-serif;background:#0d0f13;color:#e8e9ed;min-height:100vh;padding:20px}
h1{font-size:1.4rem;margin-bottom:4px}
.sub{color:#6b7e;font-size:.82rem;margin-bottom:20px}
.top{display:flex;gap:12px;align-items:center;margin-bottom:20px;flex-wrap:wrap}
.btn{padding:8px 18px;border:none;border-radius:8px;font-weight:600;font-size:.82rem;cursor:pointer}
.btn-amber{background:#f5a623;color:#0d0f13}
.btn-blue{background:#3b82f6;color:#fff}
.btn-green{background:#22c55e;color:#fff}
.btn-red{background:#ef4444;color:#fff}
.btn-gray{background:#2a2d38;color:#a0a3ae}
.stats{display:flex;gap:12px;margin-bottom:20px;flex-wrap:wrap}
.stat{background:#14161c;border:1px solid #2a2d38;border-radius:10px;padding:12px 18px}
.stat-n{font-size:1.5rem;font-weight:700;color:#f5a623}.stat-l{font-size:.7rem;color:#6b7e;margin-top:2px}
table{width:100%;border-collapse:collapse;background:#14161c;border-radius:12px;overflow:hidden}
th{background:#1a1d25;padding:10px 12px;font-size:.72rem;text-transform:uppercase;color:#6b7e;text-align:left;font-weight:600}
td{padding:10px 12px;border-top:1px solid #1a1d25;font-size:.82rem}
tr:hover td{background:#1a1d25}
.img-thumb{width:50px;height:35px;object-fit:cover;border-radius:4px}
input,textarea,select{background:#1a1d25;border:1px solid #2a2d38;border-radius:6px;padding:8px 10px;color:#e8e9ed;font-size:.82rem;font-family:inherit}
input:focus,textarea:focus{border-color:#f5a623;outline:none}
.form-row{display:flex;gap:10px;margin-bottom:10px;align-items:center;flex-wrap:wrap}
.form-row label{width:100px;font-size:.75rem;color:#6b7e;flex-shrink:0}
.form-row input,.form-row textarea,.form-row select{flex:1;min-width:200px}
.modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:100;align-items:center;justify-content:center}
.modal.open{display:flex}
.modal-box{background:#14161c;border:1px solid #2a2d38;border-radius:12px;padding:24px;width:600px;max-width:95vw;max-height:90vh;overflow-y:auto}
.modal h2{font-size:1.1rem;margin-bottom:16px}
.tag{display:inline-block;font-size:.65rem;padding:2px 8px;border-radius:4px;font-weight:600}
.tag-w{background:rgba(59,130,246,.15);color:#3b82f6}
.tag-l{background:rgba(16,185,129,.15);color:#10b981}
.toast{position:fixed;bottom:20px;right:20px;background:#22c55e;color:#fff;padding:12px 20px;border-radius:8px;font-weight:600;display:none;z-index:200}
input,select,textarea{background:#0b0d14!important;color:#e2e8f0!important;border:1px solid #1e293b!important;border-radius:8px!important}input::placeholder{color:#475569!important}</style>
<link rel="stylesheet" href="/assets/dark-iframe.css">
<!-- DOCTRINE-60-UX-ENRICH products-batch-doctrine195 -->
<style id="wtp-doctrine60-ux-premium">
:root {
--wtp-bg-start:#0a0f1c; --wtp-bg-end:#0f172a;
--wtp-surface:rgba(15,23,42,.85); --wtp-surface-hover:rgba(30,41,59,.9);
--wtp-border:rgba(99,102,241,.25); --wtp-border-hover:rgba(99,102,241,.5);
--wtp-text:#e2e8f0; --wtp-text-dim:#94a3b8; --wtp-text-bright:#f1f5f9;
--wtp-primary:#6366f1; --wtp-primary-hover:#7c7feb;
--wtp-accent:#8b5cf6; --wtp-success:#10b981; --wtp-warning:#f59e0b; --wtp-danger:#ef4444;
--wtp-radius:12px; --wtp-shadow:0 4px 24px rgba(99,102,241,.15); --wtp-shadow-lg:0 8px 48px rgba(99,102,241,.25);
--wtp-transition:all .2s cubic-bezier(.4,0,.2,1);
--wtp-font:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;
--wtp-font-mono:'JetBrains Mono',monospace;
}
.wtp-card{background:var(--wtp-surface);border:1px solid var(--wtp-border);border-radius:var(--wtp-radius);padding:20px;transition:var(--wtp-transition)}
.wtp-card:hover{border-color:var(--wtp-border-hover);box-shadow:var(--wtp-shadow)}
.wtp-btn{background:linear-gradient(135deg,var(--wtp-primary),var(--wtp-accent));color:#fff;padding:10px 20px;border:none;border-radius:8px;cursor:pointer;font-weight:600;transition:var(--wtp-transition)}
.wtp-btn:hover{transform:translateY(-1px);box-shadow:var(--wtp-shadow)}
.wtp-badge{display:inline-flex;align-items:center;padding:4px 10px;background:var(--wtp-surface);border:1px solid var(--wtp-border);border-radius:20px;font-size:12px;color:var(--wtp-text-dim)}
@media (max-width:768px){#weval-bot-widget{bottom:100px !important;right:16px !important;z-index:10001 !important}#weval-bot-btn{width:48px !important;height:48px !important}#weval-bot-btn svg{width:22px !important;height:22px !important}#footer_banner,.footer-banner,[class*="footer-bandeau"]{z-index:9990 !important}}
</style>
<!-- DOCTRINE-201-GEMINI-APPLY-20260424-181436 -->
<style>
:root {
--wtp-bg: #1A1D24; /* Dark background */
--wtp-card: #242830; /* Slightly lighter card background */
--wtp-primary: #FFC107; /* Orange/yellow for primary actions */
--wtp-accent: #66BB6A; /* Vibrant green for accents */
--wtp-text-light: #E0E0E0;
--wtp-text-muted: #A0A0A0;
--wtp-border: #3A3F47;
--wtp-shadow: rgba(0, 0, 0, 0.3);
--wtp-shadow-light: rgba(0, 0, 0, 0.15);
}
body {
font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: var(--wtp-bg);
color: var(--wtp-text-light);
margin: 0;
padding: 20px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* General container styling */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 15px;
}
/* Header styling */
h1, h2, h3, h4, h5, h6 {
color: var(--wtp-text-light);
margin-top: 0;
margin-bottom: 15px;
}
/* Buttons */
button, .button-like {
padding: 10px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 8px;
text-decoration: none;
color: inherit;
}
/* Specific elements from the image */
.header-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid var(--wtp-border);
}
.header-title {
display: flex;
align-items: center;
gap: 15px;
}
.header-title img {
height: 30px; /* Adjust as needed */
}
.header-title h1 {
font-size: 1.8rem;
font-weight: 700;
}
.header-subtitle {
color: var(--wtp-text-muted);
font-size: 0.9rem;
margin-top: -10px;
margin-bottom: 20px;
}
.header-actions {
display: flex;
gap: 10px;
}
.header-actions button, .header-actions .button-like {
background-color: var(--wtp-card);
color: var(--wtp-text-light);
border: 1px solid var(--wtp-border);
box-shadow: 0 2px 5px var(--wtp-shadow-light);
}
.header-actions button:hover, .header-actions .button-like:hover {
background-color: var(--wtp-border);
transform: translateY(-1px);
box-shadow: 0 4px 8px var(--wtp-shadow);
}
.header-actions button.primary, .header-actions .button-like.primary {
background-color: var(--wtp-primary);
color: var(--wtp-bg); /* Dark text on primary button */
box-shadow: 0 4px 10px rgba(255, 193, 7, 0.3);
}
.header-actions button.primary:hover, .header-actions .button-like.primary:hover {
background-color: #FFD54F; /* Lighter primary on hover */
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(255, 193, 7, 0.5);
}
.last-updated {
font-size: 0.85rem;
color: var(--wtp-text-muted);
text-align: right;
margin-top: -10px;
margin-bottom: 20px;
}
/* KPI Section */
.kpi-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
/* Table Styling */
.data-table-wrapper {
overflow-x: auto;
border-radius: 12px;
box-shadow: 0 8px 20px var(--wtp-shadow);
}
.data-table {
background-color: var(--wtp-card);
width: 100%;
border-collapse: collapse;
min-width: 800px; /* Ensure table content doesn't shrink too much on medium screens */
}
.data-table th, .data-table td {
padding: 15px 20px;
text-align: left;
border-bottom: 1px solid var(--wtp-border);
}
.data-table th {
background-color: var(--wtp-border);
color: var(--wtp-text-muted);
font-weight: 600;
text-transform: uppercase;
font-size: 0.85rem;
}
.data-table tbody tr {
transition: background-color 0.2s ease;
}
.data-table tbody tr:hover {
background-color: #2E333D; /* Slightly lighter on hover */
}
.data-table tbody tr:last-child td {
border-bottom: none;
}
.data-table td.image-cell img {
width: 40px;
height: 40px;
border-radius: 6px;
object-fit: cover;
}
.data-table .title-cell strong {
color: var(--wtp-text-light);
display: block;
margin-bottom: 4px;
font-weight: 600;
}
.data-table .title-cell span {
color: var(--wtp-text-muted);
font-size: 0.9rem;
}
.data-table .tag {
display: inline-block;
padding: 5px 10px;
border-radius: 6px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
white-space: nowrap;
}
.data-table .tag.life-sci {
background-color: rgba(102, 187, 106, 0.2); /* var(--wtp-accent) with transparency */
color: var(--wtp-accent);
}
.data-table .tag.weval {
background-color: rgba(66, 165, 245, 0.2); /* A blue tone */
color: #42A5F5;
}
.data-table .action-buttons {
display: flex;
gap: 8px;
}
.data-table .action-buttons button {
background-color: var(--wtp-border);
color: var(--wtp-text-muted);
width: 36px;
height: 36px;
padding: 0;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 2px 5px var(--wtp-shadow-light);
}
.data-table .action-buttons button:hover {
background-color: #4A505B;
color: var(--wtp-text-light);
transform: translateY(-1px);
box-shadow: 0 4px 8px var(--wtp-shadow);
}
/* Specific requirements */
/* .wtp-hero-premium */
.wtp-hero-premium {
background: linear-gradient(135deg, var(--wtp-bg) 0%, #0F1217 100%);
padding: 60px 0;
margin-bottom: 40px;
position: relative;
overflow: hidden;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
text-align: center;
}
.wtp-hero-premium::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml;utf8,<svg width="100%" height="100%" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><defs><pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse"><path d="M 10 0 L 0 0 0 10" fill="none" stroke="%233A3F47" stroke-width="0.5"/></pattern></defs><rect width="100%" height="100%" fill="url(%23grid)"/></svg>') repeat;
opacity: 0.05; /* Subtle backdrop */
pointer-events: none;
z-index: 0;
}
.wtp-hero-premium > * {
position: relative;
z-index: 1;
}
/* .wtp-kpi-card */
.wtp-kpi-card {
background-color: var(--wtp-card);
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 15px var(--wtp-shadow);
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 100px;
position: relative;
overflow: hidden;
border: 1px solid var(--wtp-border);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.wtp-kpi-card:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px var(--wtp-shadow);
}
.wtp-kpi-card .kpi-value {
font-size: 2.2rem;
font-weight: 700;
color: var(--wtp-primary);
margin-bottom: 5px;
}
.wtp-kpi-card .kpi-label {
font-size: 0.9rem;
color: var(--wtp-text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.wtp-kpi-card .sparkline-container {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 40px; /* Height for the sparkline */
opacity: 0.3;
pointer-events: none;
}
.wtp-kpi-card .sparkline-container svg {
width: 100%;
height: 100%;
stroke: var(--wtp-accent); /* Sparkline color */
stroke-width: 2;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
/* .wtp-status-led */
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(102, 187, 106, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(102, 187, 106, 0); }
100% { box-shadow: 0 0 0 0 rgba(102, 187, 106, 0); }
}
.wtp-status-led {
display: inline-block;
width: 10px;
height: 10px;
background-color: var(--wtp-accent);
border-radius: 50%;
margin-right: 8px;
animation: pulse 2s infinite;
vertical-align: middle;
}
/* .wtp-action-btn */
.wtp-action-btn {
background: linear-gradient(45deg, var(--wtp-primary) 0%, #FFD54F 100%);
color: var(--wtp-bg);
border: none;
padding: 12px 25px;
border-radius: 8px;
font-weight: 700;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 10px rgba(255, 193, 7, 0.3);
display: inline-flex;
align-items: center;
gap: 8px;
text-decoration: none; /* In case it's an anchor */
white-space: nowrap;
}
.wtp-action-btn:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(255, 193, 7, 0.5);
background: linear-gradient(45deg, #FFD54F 0%, var(--wtp-primary) 100%);
}
/* Media query mobile 768px (bot-widget bottom 100px anti-overlap) */
@media (max-width: 768px) {
body {
padding: 15px;
}
.header-section {
flex-direction: column;
align-items: flex-start;
gap: 20px;
}
.header-actions {
width: 100%;
justify-content: stretch;
flex-wrap: wrap;
}
.header-actions button, .header-actions .button-like {
flex-grow: 1;
padding: 12px 15px;
font-size: 0.9rem;
}
.kpi-grid {
grid-template-columns: 1fr;
}
.data-table-wrapper {
border-radius: 8px;
}
.data-table th, .data-table td {
padding: 12px 15px;
}
.data-table .title-cell strong {
font-size: 0.95rem;
}
.data-table .title-cell span {
font-size: 0.8rem;
}
.data-table .tag {
font-size: 0.75rem;
padding: 4px 8px;
}
.data-table .action-buttons button {
width: 32px;
height: 32px;
}
/* Assuming a '.bot-widget' class for the bottom widget */
.bot-widget {
position: fixed;
bottom: 100px; /* Anti-overlap with potential mobile navigation/footer */
left: 0;
right: 0;
z-index: 1000;
background-color: var(--wtp-card);
padding: 15px;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
box-shadow: 0 -4px 15px rgba(0,0,0,0.3);
}
}
/* General improvements for premium feel */
a {
color: var(--wtp-primary);
text-decoration: none;
transition: color 0.2s ease;
}
a:hover {
color: #FFD54F;
}
/* Scrollbar styling for a dark theme */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--wtp-bg);
}
::-webkit-scrollbar-thumb {
background: var(--wtp-border);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--wtp-text-muted);
}
</style>
<!-- END-DOCTRINE-201 -->
</head>
<body>
<h1>📰 LinkedIn Posts Manager</h1>
<div class="sub">Gérez vos publications LinkedIn — les stats se mettent à jour en temps réel sur la page Actualités</div>
<div class="top">
<button class="btn btn-amber" onclick="openAdd()"> Ajouter un post</button>
<button class="btn btn-blue" onclick="openImport()">🔗 Importer depuis URL</button>
<button class="btn btn-gray" onclick="loadPosts()">🔄 Rafraîchir</button>
<span style="margin-left:auto;font-size:.75rem;color:#6b7e" id="last-update"></span>
</div>
<div class="stats" id="stats"></div>
<table>
<thead>
<tr>
<th>Image</th>
<th>Date</th>
<th>Titre</th>
<th>Source</th>
<th>👍</th>
<th>💬</th>
<th>🔁</th>
<th>👁</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="posts-table"></tbody>
</table>
<!-- Add/Edit Modal -->
<div class="modal" id="modal-add">
<div class="modal-box">
<h2 id="modal-title"> Ajouter un post LinkedIn</h2>
<input type="hidden" id="edit-id">
<div class="form-row"><label>URL LinkedIn</label><input type="text" id="f-url" placeholder="https://www.linkedin.com/feed/update/urn:li:activity:..." oninput="parseUrl(this.value)"></div>
<div class="form-row"><label>Date</label><input type="date" id="f-date"></div>
<div class="form-row"><label>Titre</label><input type="text" id="f-title" placeholder="Titre du post"></div>
<div class="form-row"><label>Description</label><textarea id="f-excerpt" rows="2" placeholder="Texte court du post"></textarea></div>
<div class="form-row"><label>Image</label><input type="text" id="f-image" placeholder="/uploads/actualites/mon-image.jpg"></div>
<div class="form-row"><label>Source</label><select id="f-source"><option value="W">WEVAL</option><option value="L">Life Sciences</option></select></div>
<div class="form-row"><label>Likes</label><input type="number" id="f-likes" value="0" min="0"></div>
<div class="form-row"><label>Comments</label><input type="number" id="f-comments" value="0" min="0"></div>
<div class="form-row"><label>Reposts</label><input type="number" id="f-reposts" value="0" min="0"></div>
<div class="form-row"><label>Vues</label><input type="number" id="f-views" value="0" min="0"></div>
<div style="display:flex;gap:8px;margin-top:16px">
<button class="btn btn-green" onclick="savePost()">💾 Enregistrer</button>
<button class="btn btn-gray" onclick="closeModal('modal-add')">Annuler</button>
</div>
</div>
</div>
<!-- Import Modal -->
<div class="modal" id="modal-import">
<div class="modal-box">
<h2>🔗 Importer depuis un lien LinkedIn</h2>
<p style="font-size:.82rem;color:#6b7e;margin-bottom:12px">Collez le lien de votre post LinkedIn. Les infos seront pré-remplies.</p>
<div class="form-row"><label>URL</label><input type="text" id="import-url" placeholder="https://www.linkedin.com/feed/update/..." style="flex:2"></div>
<div class="form-row"><label>Titre</label><input type="text" id="import-title" placeholder="Titre du post (copié depuis LinkedIn)"></div>
<div class="form-row"><label>Description</label><textarea id="import-excerpt" rows="2" placeholder="Première phrase du post"></textarea></div>
<div style="display:flex;gap:8px;margin-top:16px">
<button class="btn btn-blue" onclick="importPost()">📥 Importer</button>
<button class="btn btn-gray" onclick="closeModal('modal-import')">Annuler</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
const API='/api/linkedin-posts.php';
let allPosts=[];
function toast(msg){const t=document.getElementById('toast');t.textContent=msg;t.style.display='block';setTimeout(()=>t.style.display='none',3000)}
async function loadPosts(){
const d=await fetch(API+'?action=list').then(r=>r.json());
allPosts=d.posts||[];
document.getElementById('last-update').textContent='Mis à jour: '+new Date().toLocaleTimeString();
// Stats
const total=allPosts.length;
const totalLikes=allPosts.reduce((s,p)=>s+parseInt(p.likes||0),0);
const totalViews=allPosts.reduce((s,p)=>s+parseInt(p.views||0),0);
const weval=allPosts.filter(p=>p.source==='W').length;
const ls=allPosts.filter(p=>p.source==='L').length;
document.getElementById('stats').innerHTML=`
<div class="stat"><div class="stat-n">${total}</div><div class="stat-l">Posts</div></div>
<div class="stat"><div class="stat-n">${totalLikes}</div><div class="stat-l">Total Likes</div></div>
<div class="stat"><div class="stat-n">${totalViews>999?(totalViews/1000).toFixed(1)+'K':totalViews}</div><div class="stat-l">Total Vues</div></div>
<div class="stat"><div class="stat-n">${weval}</div><div class="stat-l">WEVAL</div></div>
<div class="stat"><div class="stat-n">${ls}</div><div class="stat-l">Life Sciences</div></div>`;
// Table
const tb=document.getElementById('posts-table');
tb.innerHTML=allPosts.map(p=>`<tr>
<td>${p.image?'<img src="'+p.image+'" class="img-thumb" onerror="this.style.display=\'none\'">':'-'}</td>
<td style="white-space:nowrap">${p.post_date||''}</td>
<td style="max-width:300px"><b>${esc(p.title)}</b><br><span style="font-size:.72rem;color:#6b7e">${esc(p.excerpt||'').substring(0,60)}</span></td>
<td><span class="tag tag-${(p.source||'w').toLowerCase()}">${p.source==='L'?'Life Sci':'WEVAL'}</span></td>
<td><input type="number" value="${p.likes||0}" min="0" style="width:55px" onchange="updateStat(${p.id},'likes',this.value)"></td>
<td><input type="number" value="${p.comments||0}" min="0" style="width:55px" onchange="updateStat(${p.id},'comments',this.value)"></td>
<td><input type="number" value="${p.reposts||0}" min="0" style="width:55px" onchange="updateStat(${p.id},'reposts',this.value)"></td>
<td><input type="number" value="${p.views||0}" min="0" style="width:65px" onchange="updateStat(${p.id},'views',this.value)"></td>
<td style="white-space:nowrap">
<button class="btn btn-gray" style="padding:4px 8px;font-size:.7rem" onclick="editPost(${p.id})">✏️</button>
${p.linkedin_url?'<a href="'+p.linkedin_url+'" target="_blank" class="btn btn-gray" style="padding:4px 8px;font-size:.7rem;text-decoration:none;display:inline-block">🔗</a>':''}
</td>
</tr>`).join('');
}
function esc(s){const d=document.createElement('div');d.textContent=s||'';return d.innerHTML}
async function updateStat(id,field,val){
const body={id:id};body[field]=parseInt(val)||0;
await fetch(API+'?action=update',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
toast('✅ '+field+' mis à jour');
}
function openAdd(){
document.getElementById('modal-title').textContent=' Ajouter un post';
document.getElementById('edit-id').value='';
['f-url','f-title','f-excerpt','f-image'].forEach(id=>document.getElementById(id).value='');
document.getElementById('f-date').value=new Date().toISOString().split('T')[0];
document.getElementById('f-source').value='W';
['f-likes','f-comments','f-reposts','f-views'].forEach(id=>document.getElementById(id).value='0');
document.getElementById('modal-add').classList.add('open');
}
function editPost(id){
const p=allPosts.find(x=>x.id==id);if(!p)return;
document.getElementById('modal-title').textContent='✏️ Modifier le post';
document.getElementById('edit-id').value=id;
document.getElementById('f-url').value=p.linkedin_url||'';
document.getElementById('f-date').value=p.post_date||'';
document.getElementById('f-title').value=p.title||'';
document.getElementById('f-excerpt').value=p.excerpt||'';
document.getElementById('f-image').value=p.image||'';
document.getElementById('f-source').value=p.source||'W';
document.getElementById('f-likes').value=p.likes||0;
document.getElementById('f-comments').value=p.comments||0;
document.getElementById('f-reposts').value=p.reposts||0;
document.getElementById('f-views').value=p.views||0;
document.getElementById('modal-add').classList.add('open');
}
async function savePost(){
const id=document.getElementById('edit-id').value;
const data={
date:document.getElementById('f-date').value,
title:document.getElementById('f-title').value,
excerpt:document.getElementById('f-excerpt').value,
image:document.getElementById('f-image').value,
source:document.getElementById('f-source').value,
likes:parseInt(document.getElementById('f-likes').value)||0,
comments:parseInt(document.getElementById('f-comments').value)||0,
reposts:parseInt(document.getElementById('f-reposts').value)||0,
views:parseInt(document.getElementById('f-views').value)||0,
linkedin_url:document.getElementById('f-url').value
};
if(!data.title){toast('❌ Titre requis');return}
if(id){
data.id=parseInt(id);
await fetch(API+'?action=update',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});
}else{
await fetch(API+'?action=add',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});
}
closeModal('modal-add');
toast('✅ Post '+(id?'modifié':'ajouté'));
loadPosts();
}
function openImport(){document.getElementById('modal-import').classList.add('open')}
async function importPost(){
const url=document.getElementById('import-url').value;
const title=document.getElementById('import-title').value;
const excerpt=document.getElementById('import-excerpt').value;
if(!title){toast('❌ Titre requis');return}
await fetch(API+'?action=add',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
date:new Date().toISOString().split('T')[0],
title:title,
excerpt:excerpt,
linkedin_url:url,
source:'W',
likes:0,comments:0,reposts:0,views:0,
image:'/uploads/actualites/img-wevia-ia.jpg'
})});
closeModal('modal-import');
toast('✅ Post importé');
loadPosts();
}
function parseUrl(url){
// Extract post ID from LinkedIn URL for future API use
const m=url.match(/activity:(\d+)/);
if(m)console.log('LinkedIn activity ID:',m[1]);
}
function closeModal(id){document.getElementById(id).classList.remove('open')}
document.querySelectorAll('.modal').forEach(m=>m.addEventListener('click',e=>{if(e.target===m)m.classList.remove('open')}));
loadPosts();
</script>
</body>
</html>