120 120 < input type ="password " class ="l-input " id ="lp " placeholder ="Mot de passe " onkeydown ="if(event.key==='Enter')doLogin() ">
121 121 < button class ="l-btn " onclick ="doLogin() "> Connexion →</ button >
122 122 < div class ="l-err " id ="l-err "> Identifiants incorrects</ div >
123 - < div class ="l-hint "> Démo : admin / hopon2025</ div >
123 + < div class ="l-hint "> Démo : admin / hopon2025 · < a href =" # " onclick =" resetAdminOverrides();return false; " style =" color:var(--blue) " > réinitialiser </ a > </ div >
124 124 </ div >
125 125 </ div >
126 126
@@ -371,26 +371,98 @@ <h3>Catalogue — statut des prix</h3>
371 371 </ div > <!-- /admin -->
372 372
373 373 < script data-cfasync ="false " src ="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js "> </ script > < script >
374 + const ADMIN_AUTH_KEY = 'hopon_admin_auth' ;
375 + const ADMIN_SECRET_KEY = 'hopon_admin_secret' ;
376 + const ADMIN_API_BASE = ( location . origin || '' ) + '/api/v1/admin' ;
377 +
378 + function getAdminSecret ( ) {
379 + try { return ( localStorage . getItem ( ADMIN_SECRET_KEY ) || '' ) . trim ( ) ; } catch ( e ) { return '' ; }
380 + }
381 +
382 + async function adminFetch ( path , options ) {
383 + const opts = options || { } ;
384 + const headers = Object . assign ( { } , opts . headers || { } ) ;
385 + const secret = getAdminSecret ( ) ;
386 + if ( secret ) headers [ 'x-admin-secret' ] = secret ;
387 + const res = await fetch ( ADMIN_API_BASE + path , Object . assign ( { } , opts , { headers} ) ) ;
388 + if ( res . status === 401 ) {
389 + const entered = prompt ( 'Secret admin requis (ADMIN_SECRET). Laisser vide si non configuré :' , '' ) ;
390 + if ( entered !== null ) {
391 + try { localStorage . setItem ( ADMIN_SECRET_KEY , entered . trim ( ) ) ; } catch ( e ) { }
392 + return adminFetch ( path , options ) ;
393 + }
394 + }
395 + if ( ! res . ok ) {
396 + let msg = 'Erreur API admin' ;
397 + try { const j = await res . json ( ) ; if ( j && j . error ) msg = j . error ; } catch ( e ) { }
398 + throw new Error ( msg ) ;
399 + }
400 + return res . json ( ) ;
401 + }
402 +
403 + function getAdminCredentials ( ) {
404 + const defaultCreds = { u :'admin' , p :'hopon2025' } ;
405 + const storedUser = ( localStorage . getItem ( 'hopon_admin_user' ) || '' ) . trim ( ) ;
406 + const storedPass = ( localStorage . getItem ( 'hopon_admin_pass' ) || '' ) . trim ( ) ;
407 + const list = [ defaultCreds ] ;
408 + if ( storedUser && storedPass ) {
409 + list . push ( { u :storedUser , p :storedPass } ) ;
410 + }
411 + return list ;
412 + }
413 +
414 + function openAdminSession ( ) {
415 + document . getElementById ( 'login' ) . style . display = 'none' ;
416 + document . getElementById ( 'admin' ) . classList . add ( 'on' ) ;
417 + document . getElementById ( 'admin-date' ) . textContent = new Date ( ) . toLocaleDateString ( 'fr-FR' , { weekday :'long' , day :'numeric' , month :'long' } ) ;
418 + try { localStorage . setItem ( ADMIN_AUTH_KEY , '1' ) ; } catch ( e ) { }
419 + buildAll ( ) ;
420 + }
421 +
374 422 function doLogin ( ) {
375 - const u=document.getElementById('lu').value;
376 - const p=document.getElementById('lp').value;
377 - if(u==='admin'&&p==='hopon2025'){
378 - document.getElementById('login').style.display='none';
379 - document.getElementById('admin').classList.add('on');
380 - document.getElementById('admin-date').textContent=new Date().toLocaleDateString('fr-FR',{weekday:'long',day:'numeric',month:'long'});
381 - buildAll();
423 + const inputUser = ( document . getElementById ( 'lu' ) . value || '' ) . trim ( ) . toLowerCase ( ) ;
424 + const inputPass = ( document . getElementById ( 'lp' ) . value || '' ) . trim ( ) ;
425 + const creds = getAdminCredentials ( ) ;
426 + const ok = creds . some ( function ( c ) {
427 + return inputUser === c . u . toLowerCase ( ) && inputPass === c . p ;
428 + } ) ;
429 +
430 + if ( ok ) {
431 + openAdminSession ( ) ;
382 432 } else {
383 - const err=document.getElementById('l-err');err.style.display='block';
384 - setTimeout(()=>err.style.display='none',3000);
433 + const err = document . getElementById ( 'l-err' ) ;
434 + err . textContent = 'Identifiants incorrects (essayez admin / hopon2025)' ;
435 + err . style . display = 'block' ;
436 + setTimeout ( ( ) => err . style . display = 'none' , 4000 ) ;
385 437 }
386 438 }
387 439 function doLogout ( ) {
388 440 document . getElementById ( 'admin' ) . classList . remove ( 'on' ) ;
389 441 document . getElementById ( 'login' ) . style . display = 'flex' ;
390 442 document . getElementById ( 'lu' ) . value = '' ;
391 443 document . getElementById ( 'lp' ) . value = '' ;
444 + try { localStorage . removeItem ( ADMIN_AUTH_KEY ) ; } catch ( e ) { }
445 + }
446 +
447 +
448 + function resetAdminOverrides ( ) {
449 + try {
450 + localStorage . removeItem ( 'hopon_admin_user' ) ;
451 + localStorage . removeItem ( 'hopon_admin_pass' ) ;
452 + localStorage . removeItem ( ADMIN_AUTH_KEY ) ;
453 + } catch ( e ) { }
454 + const err = document . getElementById ( 'l-err' ) ;
455 + err . textContent = 'Configuration admin réinitialisée. Reconnectez-vous.' ;
456 + err . style . display = 'block' ;
457 + setTimeout ( ( ) => err . style . display = 'none' , 3000 ) ;
392 458 }
393 459
460 + ( function initAdminAuth ( ) {
461 + try {
462 + if ( localStorage . getItem ( ADMIN_AUTH_KEY ) === '1' ) openAdminSession ( ) ;
463 + } catch ( e ) { }
464 + } ) ( ) ;
465 +
394 466 const PAGE_TITLES = { overview :'Dashboard' , orders :'Commandes' , esim :'eSIM Stock' , prices :'Gestion des prix' , sync :'Sync API fournisseur' , storylines :'Visuels & Narration' , woo :'Architecture WooCommerce' , webhooks :'Webhooks & Logs' , clients :'Clients' , 'pro-admin' :'Comptes Pro (Admin)' , seo :'Pages SEO' , faq :'FAQ & Guides' } ;
395 467 function showPage ( id , el ) {
396 468 document . querySelectorAll ( '.page' ) . forEach ( p => p . classList . remove ( 'on' ) ) ;
@@ -408,42 +480,113 @@ <h3>Catalogue — statut des prix</h3>
408 480 { id :'#4820' , cl :'marie.d@acme.fr' , dest :'🇺🇸 USA' , plan :'14j·Illimité' , sid :'OCS-US-004' , iccid :'—' , pub :'—' , sup :'—' , st :'b-pend' , lb :'Non sync' } ,
409 481 { id :'#4819' , cl :'lucas.b@email.com' , dest :'🇲🇦 Maroc' , plan :'7j·3GB' , sid :'OCS-MA-001' , iccid :'—' , pub :'—' , sup :'—' , st :'b-draft' , lb :'Brouillon' } ,
410 482 ] ;
411 - const tpl=r=>`<tr><td>${r.id}</td><td style="font-size:11px">${r.cl}</td><td>${r.dest}</td><td>${r.plan}</td><td><code>${r.sid}</code></td><td style="font-family:monospace;font-size:10px;color:var(--txtd)">${r.iccid}</td><td style="color:var(--txtd);font-style:italic">${r.pub}</td><td style="color:var(--txtd);font-style:italic">${r.sup}</td><td><span class="badge ${r.st}">${r.lb}</span></td></tr>`;
483 + function tpl ( r ) {
484 + return '<tr>'
485 + + '<td>' + r . id + '</td>'
486 + + '<td style="font-size:11px">' + r . cl + '</td>'
487 + + '<td>' + r . dest + '</td>'
488 + + '<td>' + r . plan + '</td>'
489 + + '<td><code>' + r . sid + '</code></td>'
490 + + '<td style="font-family:monospace;font-size:10px;color:var(--txtd)">' + r . iccid + '</td>'
491 + + '<td style="color:var(--txtd);font-style:italic">' + r . pub + '</td>'
492 + + '<td style="color:var(--txtd);font-style:italic">' + r . sup + '</td>'
493 + + '<td><span class="badge ' + r . st + '">' + r . lb + '</span></td>'
494 + + '</tr>' ;
495 + }
412 496 const b1 = document . getElementById ( 'o-tbody' ) , b2 = document . getElementById ( 'o2-tbody' ) ;
413 497 if ( b1 ) b1 . innerHTML = rows . map ( tpl ) . join ( '' ) ;
414 498 if ( b2 ) b2 . innerHTML = rows . map ( tpl ) . join ( '' ) ;
415 499 }
416 500
417 501 function buildPrices ( ) {
418 502 const c = document . getElementById ( 'price-rows' ) ; if ( ! c ) return ;
419 - const items=[
420 - {co:'🇯🇵 Japon',pr:'7j · 5 GB',sid:'OCS-JP-001',st:'not_synced'},
421 - {co:'🇯🇵 Japon',pr:'7j · 10 GB',sid:'OCS-JP-002',st:'not_synced'},
422 - {co:'🇯🇵 Japon',pr:'7j · Illimité',sid:'OCS-JP-003',st:'not_synced'},
423 - {co:'🇲🇦 Maroc',pr:'7j · 3 GB',sid:'OCS-MA-001',st:'not_synced'},
424 - {co:'🇲🇦 Maroc',pr:'7j · Illimité',sid:'OCS-MA-002',st:'api_error'},
425 - ];
503 + c . innerHTML = '<div class="log-row"><span class="log-t">…</span><div><div class="log-msg">Chargement produits réels…</div></div></div>' ;
426 504 const sl = { not_synced :'b-draft' , api_error :'b-err' , pending_review :'b-pend' , validated :'b-ok' } ;
427 505 const ll = { not_synced :'Non synchronisé' , api_error :'Erreur API — prix supprimé' , pending_review :'En attente admin' , validated :'Validé' } ;
428 - c.innerHTML=items.map(p=>`<div class="pval-row"><div style="width:80px;color:var(--txt);font-size:12px">${p.co}</div><div style="width:100px;color:var(--txtm);font-size:12px">${p.pr}</div><div style="flex:1"><code>${p.sid}</code></div><div style="width:60px;color:var(--txtd);font-size:11px;font-style:italic">—</div><div style="width:70px;color:var(--txtd);font-size:11px;font-style:italic">—</div><div style="width:120px"><span class="badge ${sl[p.st]}">${ll[p.st]}</span></div><div style="display:flex;gap:4px"><button class="btn btn-blue btn-sm" title="Sync">🔄</button><button class="btn btn-sm" disabled style="opacity:.4" title="Valider">✓</button></div></div>`).join('');
506 + adminFetch ( '/products' )
507 + . then ( function ( items ) {
508 + if ( ! Array . isArray ( items ) || items . length === 0 ) {
509 + c . innerHTML = '<div class="log-row"><span class="log-t">—</span><div><div class="log-msg">Aucun produit renvoyé par l’API admin.</div></div></div>' ;
510 + return ;
511 + }
512 + c . innerHTML = items . map ( function ( p ) {
513 + const st = p . price_status || 'not_synced' ;
514 + const co = ( p . country_iso2 || '—' ) ;
515 + const plan = ( p . name || 'Produit' ) ;
516 + const sid = ( p . supplier_id || p . id || '—' ) ;
517 + const sup = ( p . supplier_price != null ) ?Number ( p . supplier_price ) . toFixed ( 2 ) + '€' :'—' ;
518 + const pub = ( p . public_price != null ) ?Number ( p . public_price ) . toFixed ( 2 ) + '€' :'—' ;
519 + return `<div class="pval-row"><div style="width:80px;color:var(--txt);font-size:12px">${ co } </div><div style="width:160px;color:var(--txtm);font-size:12px">${ plan } </div><div style="flex:1"><code>${ sid } </code></div><div style="width:60px;color:var(--txtd);font-size:11px">${ sup } </div><div style="width:70px;color:var(--txtd);font-size:11px">${ pub } </div><div style="width:120px"><span class="badge ${ sl [ st ] || 'b-draft' } ">${ ll [ st ] || st } </span></div><div style="display:flex;gap:4px"><button class="btn btn-blue btn-sm" onclick="trigSync()" title="Sync">🔄</button></div></div>` ;
520 + } ) . join ( '' ) ;
521 + } )
522 + . catch ( function ( err ) {
523 + c . innerHTML = `<div class="log-row"><span class="log-t">⚠️</span><div><div class="log-msg">${ err . message } </div><div class="log-src">Mode démo conservé (données locales)</div></div></div>` ;
524 + } ) ;
429 525 }
430 526
431 527 function trigSync ( ) {
432 528 if ( ! confirm ( 'Déclencher une synchronisation API ? Les nouveaux prix seront en statut pending_review.' ) ) return ;
433 - alert('Sync lancée. Consultez les logs pour le résultat. Les prix avec supplier_price valide passeront en pending_review pour validation admin.');
529 + adminFetch ( '/sync/catalog' , { method :'POST' } )
530 + . then ( function ( r ) {
531 + alert ( 'Sync lancée ✅ Produits traités: ' + ( r . count || 0 ) ) ;
532 + buildPrices ( ) ;
533 + buildSyncLogs ( ) ;
534 + } )
535 + . catch ( function ( err ) {
536 + alert ( 'Erreur sync: ' + err . message ) ;
537 + } ) ;
434 538 }
435 539
436 540 function buildSyncLogs ( ) {
437 541 const c = document . getElementById ( 'sync-logs' ) ; if ( ! c ) return ;
438 - const logs=[
439 - {t:'12:04:22',col:'var(--green)',m:'Sync OCS démarrée',src:'GET /ocs/catalog/api/cos/COS001/produits'},
440 - {t:'12:04:24',col:'var(--green)',m:'342 produits reçus — mapping en cours',src:''},
441 - {t:'12:04:25',col:'var(--ora)',m:'5 produits supplier_price=null → statut not_synced',src:'OCS-JP-001, OCS-JP-002, OCS-JP-003, OCS-MA-001, OCS-MA-002'},
442 - {t:'12:04:25',col:'var(--red)',m:'ALERTE: OCS-MA-002 (Maroc Illimité 7j) supplier_price=null → prix public retiré du site',src:'OCS-MA-002 · ancien prix 19,90€ supprimé'},
443 - {t:'12:04:26',col:'var(--green)',m:'Sync terminée — 337 produits publiés, 5 en not_synced/api_error',src:''},
444 - ];
445 - c.innerHTML=logs.map(l=>`<div class="log-row"><span class="log-t">${l.t}</span><div class="log-dot" style="background:${l.col}"></div><div><div class="log-msg">${l.m}</div>${l.src?`<div class="log-src">${l.src}</div>`:''} </div></div>`).join('');
542 + c . innerHTML = '<div class="log-row"><span class="log-t">…</span><div><div class="log-msg">Chargement logs API…</div></div></div>' ;
543 + adminFetch ( '/logs' )
544 + . then ( function ( logs ) {
545 + if ( ! Array . isArray ( logs ) || logs . length === 0 ) {
546 + c . innerHTML = '<div class="log-row"><span class="log-t">—</span><div><div class="log-msg">Aucun log disponible.</div></div></div>' ;
547 + return ;
548 + }
549 + c . innerHTML = logs . slice ( 0 , 20 ) . map ( function ( l ) {
550 + const t = l . created_at ? new Date ( l . created_at ) . toLocaleTimeString ( 'fr-FR' ) : '—' ;
551 + const msg = l . message || l . endpoint || 'Log API' ;
552 + const lvl = ( l . level || '' ) . toLowerCase ( ) ;
553 + const col = lvl === 'error' ?'var(--red)' :( lvl === 'warn' ?'var(--ora)' :'var(--green)' ) ;
554 + const src = [ l . method , l . endpoint ] . filter ( Boolean ) . join ( ' ' ) ;
555 + return `<div class="log-row"><span class="log-t">${ t } </span><div class="log-dot" style="background:${ col } "></div><div><div class="log-msg">${ msg } </div>${ src ?`<div class="log-src">${ src } </div>` :'' } </div></div>` ;
556 + } ) . join ( '' ) ;
557 + } )
558 + . catch ( function ( err ) {
559 + c . innerHTML = `<div class="log-row"><span class="log-t">⚠️</span><div><div class="log-msg">${ err . message } </div></div></div>` ;
560 + } ) ;
446 561 }
447 562
448 563 function buildWHLogs ( ) {
449 - const c=document.getElementById('wh-logs')
564 + const c = document . getElementById ( 'wh-logs' ) ; if ( ! c ) return ;
565 + c . innerHTML = '<div class="log-row"><span class="log-t">…</span><div><div class="log-msg">Chargement webhooks/stripe…</div></div></div>' ;
566 + adminFetch ( '/logs' )
567 + . then ( function ( logs ) {
568 + const filtered = ( Array . isArray ( logs ) ?logs :[ ] ) . filter ( function ( l ) {
569 + const m = ( ( l . message || '' ) + ' ' + ( l . endpoint || '' ) ) . toLowerCase ( ) ;
570 + return m . includes ( 'webhook' ) || m . includes ( 'stripe' ) || m . includes ( '/stripe/' ) ;
571 + } ) . slice ( 0 , 20 ) ;
572 + if ( filtered . length === 0 ) {
573 + c . innerHTML = '<div class="log-row"><span class="log-t">—</span><div><div class="log-msg">Aucun log Stripe/Webhook pour le moment.</div></div></div>' ;
574 + return ;
575 + }
576 + c . innerHTML = filtered . map ( function ( l ) {
577 + const t = l . created_at ? new Date ( l . created_at ) . toLocaleTimeString ( 'fr-FR' ) : '—' ;
578 + const lvl = ( l . level || '' ) . toLowerCase ( ) ;
579 + const ok = lvl !== 'error' ;
580 + const msg = l . message || l . endpoint || 'Webhook' ;
581 + const src = [ l . method , l . endpoint ] . filter ( Boolean ) . join ( ' ' ) ;
582 + return `<div class="log-row"><span class="log-t">${ t } </span><div class="log-dot" style="background:${ ok ?'var(--green)' :'var(--red)' } "></div><div><div class="log-msg">${ msg } </div>${ src ?`<div class="log-src">${ src } </div>` :'' } </div></div>` ;
583 + } ) . join ( '' ) ;
584 + } )
585 + . catch ( function ( err ) {
586 + c . innerHTML = `<div class="log-row"><span class="log-t">⚠️</span><div><div class="log-msg">${ err . message } </div></div></div>` ;
587 + } ) ;
588 + }
589 +
590 + </ script >
591 + </ body >
592 + </ html >