297 lines
17 KiB
HTML
297 lines
17 KiB
HTML
<?php include_once("/opt/wevads-arsenal/public/api/wevads-metrics.php"); ?>
|
||
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>WEVADS - 🔑 WEVADS Key Manager — All Providers × Personas</title>
|
||
<style>
|
||
:root{--bg:#0f172a;--c:#1e293b;--b:#334155;--t:#e2e8f0;--gd:#22c55e;--rd:#ef4444;--yl:#eab308;--bl:#3b82f6;--pr:#a855f7;--m:'DM Sans',sans-serif}
|
||
*{margin:0;padding:0;box-sizing:border-box}body{font-family:var(--m);background:#060a14;color:var(--t);padding:16px}
|
||
h1{font-size:20px;margin-bottom:4px}h2{font-size:13px;color:#64748b;margin-bottom:16px}
|
||
.stats{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:16px}
|
||
.stat{background:var(--c);border-radius:10px;padding:10px 16px;border:1px solid var(--b)}
|
||
.stat .n{font-size:22px;font-weight:700}.stat .l{font-size:10px;color:#64748b}
|
||
.filters{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap}
|
||
.fbtn{padding:6px 12px;border-radius:8px;border:1px solid var(--b);background:var(--c);color:var(--t);cursor:pointer;font-size:11px;font-family:var(--m)}
|
||
.fbtn.active{border-color:var(--bl);background:#3b82f620;color:var(--bl)}
|
||
.provider{background:var(--c);border-radius:10px;margin-bottom:8px;border:1px solid var(--b);overflow:hidden}
|
||
.provider.has-keys{border-color:var(--gd)}
|
||
.phdr{display:flex;align-items:center;padding:12px 16px;cursor:pointer;gap:10px;user-select:none}
|
||
.phdr:hover{background:#0c122008}
|
||
.phdr .arrow{transition:.2s;font-size:10px}.phdr.open .arrow{transform:rotate(90deg)}
|
||
.phdr .pname{font-weight:600;font-size:14px;flex:1}
|
||
.badge{padding:2px 7px;border-radius:5px;font-size:10px;font-weight:600}
|
||
.badge.ok{background:#16a34a20;color:var(--gd)}.badge.err{background:#dc262620;color:var(--rd)}
|
||
.badge.pend{background:#ca8a0420;color:var(--yl)}.badge.free{background:#3b82f620;color:var(--bl)}
|
||
.badge.paid{background:#a855f720;color:var(--pr)}.badge.keys{background:#22c55e20;color:var(--gd)}
|
||
.pbody{display:none;padding:0 16px 12px;border-top:1px solid var(--b)}
|
||
.pbody.open{display:block}
|
||
.signup-row{display:flex;gap:8px;align-items:center;margin:10px 0;flex-wrap:wrap}
|
||
.signup-row a{color:var(--pr);font-size:12px;text-decoration:none}
|
||
.signup-row a:hover{text-decoration:underline}
|
||
.hmkey{display:flex;gap:6px;margin:8px 0;align-items:center}
|
||
.hmkey label{font-size:11px;color:#64748b;min-width:100px}
|
||
.hmkey input{flex:1;background:var(--bg);border:1px solid var(--b);border-radius:6px;padding:6px 10px;color:var(--t);font-size:11px;font-family:monospace}
|
||
.hmkey input:focus{outline:none;border-color:var(--bl)}
|
||
.btn{padding:5px 10px;border:none;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer}
|
||
.btn-t{background:var(--bl);color:#fff}.btn-s{background:var(--gd);color:#fff}.btn-x{background:var(--pr);color:#fff}
|
||
.ptable{width:100%;border-collapse:collapse;margin-top:8px;font-size:11px}
|
||
.ptable th{text-align:left;padding:6px 8px;background:var(--bg);color:#64748b;font-size:10px;font-weight:600}
|
||
.ptable td{padding:5px 8px;border-top:1px solid #1e293b50}
|
||
.ptable tr:hover{background:#0c122006}
|
||
.ptable input{width:100%;background:var(--bg);border:1px solid var(--b);border-radius:4px;padding:4px 6px;color:var(--t);font-size:10px;font-family:monospace}
|
||
.ptable input:focus{outline:none;border-color:var(--bl)}
|
||
.ptable .btn{padding:3px 6px;font-size:9px}
|
||
.res{padding:4px 8px;border-radius:4px;font-size:10px;margin-top:4px}
|
||
.res.ok{background:#16a34a15;color:var(--gd)}.res.fail{background:#dc262615;color:var(--rd)}
|
||
.bulk-row{display:flex;gap:6px;margin-top:8px}
|
||
.cnt{font-size:10px;padding:1px 5px;border-radius:4px;background:var(--bg)}
|
||
.wv-status{position:fixed;top:12px;right:140px;z-index:9998;background:rgba(52,211,153,.15);border:1px solid #34d399;border-radius:12px;padding:3px 10px;color:#34d399;font-size:10px;font-weight:700;font-family:'JetBrains Mono',monospace}
|
||
|
||
.sc,.card,[class*="stat-card"]{transition:all .25s ease;position:relative;overflow:hidden}
|
||
.sc:hover,.card:hover,[class*="stat-card"]:hover{transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,.25)}
|
||
.sc::after,.card::after{content:'';position:absolute;bottom:0;left:0;right:0;height:2px;background:var(--cy,#22d3ee);opacity:0;transition:opacity .25s}
|
||
.sc:hover::after,.card:hover::after{opacity:.7}
|
||
.btn,.button,[class*="btn-"]{transition:all .2s ease}
|
||
.btn:hover,.button:hover{transform:translateY(-1px)}
|
||
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
||
.sc,.card{animation:fadeIn .4s ease both}
|
||
.sc:nth-child(2),.card:nth-child(2){animation-delay:.05s}
|
||
.sc:nth-child(3),.card:nth-child(3){animation-delay:.1s}
|
||
.sc:nth-child(4),.card:nth-child(4){animation-delay:.15s}
|
||
.sc:nth-child(5),.card:nth-child(5){animation-delay:.2s}
|
||
.sc:nth-child(6),.card:nth-child(6){animation-delay:.25s}
|
||
</style>
|
||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=DM+Sans:wght@400;600;700&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="wevads-global.css?v1770777318">
|
||
</head>
|
||
<body>
|
||
|
||
|
||
<h1>🔑 Key Manager — All Providers × 178 Personas</h1>
|
||
<p style="font-size:12px;color:#64748b;margin:6px 0 16px;max-width:600px;line-height:1.6">Key Manager — 45 providers × 178 personas, rotation automatique des clés API.</p>
|
||
<h2>Chaque persona = 1 compte = 1 quota de tokens gratuit. Multiplie les clés pour maximiser la capacité.</h2>
|
||
<div class="stats" id="stats">Chargement...</div>
|
||
<div class="filters" id="filters"></div>
|
||
<div id="providers">Chargement des providers...</div>
|
||
|
||
<script>
|
||
const FACTORY='/api/ia-provider-factory.php';
|
||
const ROUTER='/api/provider-router.php';
|
||
|
||
let allData={};
|
||
let currentFilter='all';
|
||
|
||
async function loadAll(){
|
||
const [dash,router]=await Promise.all([
|
||
fetch(FACTORY+'?action=dashboard').then(r=>r.json()).catch(()=>({})),
|
||
fetch(ROUTER+'?action=providers').then(r=>r.json()).catch(()=>({}))
|
||
]);
|
||
|
||
allData.dashboard=dash;
|
||
allData.router=router;
|
||
allData.providers=dash.providers||{};
|
||
|
||
// Stats
|
||
let totalAccounts=0,totalKeys=0,totalActive=0,provCount=Object.keys(allData.providers).length;
|
||
for(const[k,v] of Object.entries(allData.providers)){
|
||
totalAccounts+=v.accounts||0;
|
||
totalKeys+=v.keys||0;
|
||
totalActive+=v.active||0;
|
||
}
|
||
|
||
document.getElementById('stats').innerHTML=`
|
||
<div class="stat"><div class="n" style="color:var(--bl)">${provCount}</div><div class="l">Providers</div></div>
|
||
<div class="stat"><div class="n">${totalAccounts.toLocaleString()}</div><div class="l">Total Accounts</div></div>
|
||
<div class="stat"><div class="n" style="color:var(--gd)">${totalKeys}</div><div class="l">Real Keys</div></div>
|
||
<div class="stat"><div class="n" style="color:var(--yl)">${totalAccounts-totalKeys}</div><div class="l">Need Keys</div></div>
|
||
<div class="stat"><div class="n" style="color:var(--gd)">${totalActive}</div><div class="l">Active</div></div>
|
||
`;
|
||
|
||
// Filters
|
||
document.getElementById('filters').innerHTML=`
|
||
<button class="fbtn active" onclick="setFilter('all',this)">All (${provCount})</button>
|
||
<button class="fbtn" onclick="setFilter('haskeys',this)">Has Keys (${Object.entries(allData.providers).filter(([k,v])=>v.keys>0).length})</button>
|
||
<button class="fbtn" onclick="setFilter('nokeys',this)">Needs Keys (${Object.entries(allData.providers).filter(([k,v])=>v.keys===0).length})</button>
|
||
<button class="fbtn" onclick="setFilter('free',this)">Free Tier</button>
|
||
<button class="fbtn" onclick="setFilter('paid',this)">Paid</button>
|
||
`;
|
||
|
||
renderProviders();
|
||
}
|
||
|
||
function setFilter(f,el){
|
||
currentFilter=f;
|
||
document.querySelectorAll('.fbtn').forEach(b=>b.classList.remove('active'));
|
||
el.classList.add('active');
|
||
renderProviders();
|
||
}
|
||
|
||
const SIGNUP={
|
||
'Cerebras':'https://cloud.cerebras.ai/','Groq':'https://console.groq.com/',
|
||
'DeepSeek':'https://platform.deepseek.com/','Gemini':'https://aistudio.google.com/',
|
||
'Claude':'https://console.anthropic.com/','SambaNova':'https://cloud.sambanova.ai/',
|
||
'Hyperbolic':'https://app.hyperbolic.xyz/','Mistral':'https://console.mistral.ai/',
|
||
'Cohere':'https://dashboard.cohere.com/','OpenRouter':'https://openrouter.ai/',
|
||
'Together':'https://api.together.xyz/','Fireworks':'https://fireworks.ai/',
|
||
'NVIDIA_NIM':'https://build.nvidia.com/','HuggingFace':'https://huggingface.co/settings/tokens',
|
||
'DeepInfra':'https://deepinfra.com/dash/api_keys','AI21':'https://studio.ai21.com/',
|
||
'Cloudflare':'https://dash.cloudflare.com/','Lepton':'https://www.lepton.ai/',
|
||
'Novita':'https://novita.ai/','Nebius':'https://studio.nebius.ai/',
|
||
'Perplexity':'https://perplexity.ai/settings/api','Lambda':'https://cloud.lambdalabs.com/',
|
||
'Replicate':'https://replicate.com/account/api-tokens','GLHF':'https://glhf.chat/users/settings/api',
|
||
'Chutes':'https://chutes.ai/app/api','OctoAI':'https://octoai.cloud/',
|
||
'xAI':'https://console.x.ai/','Anyscale':'https://app.endpoints.anyscale.com/'
|
||
};
|
||
const PAID_PROVIDERS=['DeepSeek_Paid','DeepSeek_R1','Claude_Pro','Claude_API','Gemini_Advanced','Gemini_API','OpenAI_Plus','OpenAI_Pro','OpenAI_API','Mistral_Paid','Cohere_Pro'];
|
||
|
||
function renderProviders(){
|
||
let entries=Object.entries(allData.providers).sort((a,b)=>{
|
||
if(b[1].keys!==a[1].keys) return b[1].keys-a[1].keys;
|
||
return b[1].accounts-a[1].accounts;
|
||
});
|
||
|
||
if(currentFilter==='haskeys') entries=entries.filter(([k,v])=>v.keys>0);
|
||
if(currentFilter==='nokeys') entries=entries.filter(([k,v])=>v.keys===0);
|
||
if(currentFilter==='free') entries=entries.filter(([k,v])=>!PAID_PROVIDERS.includes(k));
|
||
if(currentFilter==='paid') entries=entries.filter(([k,v])=>PAID_PROVIDERS.includes(k));
|
||
|
||
let h='';
|
||
for(const[name,info] of entries){
|
||
const isPaid=PAID_PROVIDERS.includes(name);
|
||
const hasKeys=info.keys>0;
|
||
const signup=SIGNUP[name.split('_')[0]]||'';
|
||
|
||
h+=`<div class="provider ${hasKeys?'has-keys':''}">
|
||
<div class="phdr" onclick="toggleProvider(this,'${name}')">
|
||
<span class="arrow">▶</span>
|
||
<span class="pname">${name}</span>
|
||
<span class="cnt">${info.accounts} comptes</span>
|
||
${hasKeys?`<span class="badge keys">${info.keys} 🔑</span>`:''}
|
||
${info.active>0?`<span class="badge ok">${info.active} ✅</span>`:''}
|
||
<span class="badge ${isPaid?'paid':'free'}">${isPaid?'💰 Paid':'🆓 Free'}</span>
|
||
</div>
|
||
<div class="pbody" id="body-${name}">
|
||
${signup?`<div class="signup-row">🔗 Signup: <a href="${signup}" target="_blank">${signup}</a></div>`:''}
|
||
<div class="hmkey">
|
||
<label>🔑 Main Key:</label>
|
||
<input id="mainkey-${name}" placeholder="Coller la clé API principale...">
|
||
<button class="btn btn-t" onclick="testMainKey('${name}')">🧪 Test</button>
|
||
<button class="btn btn-s" onclick="saveMainKey('${name}')">💾 Hamid</button>
|
||
</div>
|
||
<div id="mainres-${name}"></div>
|
||
<div class="bulk-row">
|
||
<button class="btn btn-x" onclick="loadPersonas('${name}')">👥 Charger ${info.accounts} comptes persona</button>
|
||
<button class="btn btn-t" onclick="testAllKeys('${name}')">🧪 Tester toutes les clés</button>
|
||
</div>
|
||
<div id="personas-${name}"><p style="font-size:11px;color:#64748b;margin-top:6px">Clique "Charger" pour voir les comptes persona</p></div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
document.getElementById('providers').innerHTML=h;
|
||
}
|
||
|
||
function toggleProvider(el,name){
|
||
el.classList.toggle('open');
|
||
document.getElementById('body-'+name).classList.toggle('open');
|
||
}
|
||
|
||
async function testMainKey(name){
|
||
const key=document.getElementById('mainkey-'+name).value.trim();
|
||
const div=document.getElementById('mainres-'+name);
|
||
if(!key){div.innerHTML='<div class="res fail">Colle une clé</div>';return;}
|
||
div.innerHTML='<div class="res" style="color:var(--bl)">⏳ Test...</div>';
|
||
try{
|
||
const r=await fetch(ROUTER,{method:'POST',headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify({action:'chat',provider:name.split('_')[0],message:'say OK',test_key:key})});
|
||
const d=await r.json();
|
||
div.innerHTML=d.response?
|
||
`<div class="res ok">✅ ${d.provider} OK — ${d.latency_ms}ms</div>`:
|
||
`<div class="res fail">❌ ${JSON.stringify(d).substring(0,100)}</div>`;
|
||
}catch(e){div.innerHTML=`<div class="res fail">❌ ${e.message}</div>`;}
|
||
}
|
||
|
||
async function saveMainKey(name){
|
||
const key=document.getElementById('mainkey-'+name).value.trim();
|
||
if(!key){alert('Colle une clé');return;}
|
||
try{
|
||
// Save to hamid_providers
|
||
const fd=new FormData();fd.append('action','update_provider');fd.append('provider_name',name);fd.append('api_key',key);
|
||
await fetch('/api/hamid-chef.php',{method:'POST',body:fd});
|
||
// Also to ia_provider_accounts
|
||
await fetch(FACTORY,{method:'POST',headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify({action:'create_account',provider:name,api_key:key})});
|
||
document.getElementById('mainres-'+name).innerHTML='<div class="res ok">💾 Saved to Hamid + Factory</div>';
|
||
}catch(e){document.getElementById('mainres-'+name).innerHTML=`<div class="res fail">❌ ${e.message}</div>`;}
|
||
}
|
||
|
||
async function loadPersonas(name){
|
||
const div=document.getElementById('personas-'+name);
|
||
div.innerHTML='<p style="color:var(--bl);font-size:11px">⏳ Chargement...</p>';
|
||
try{
|
||
const r=await fetch(FACTORY+'?action=scan');
|
||
const d=await r.json();
|
||
// Filter for this provider from scan results
|
||
const accounts=(d.results||[]).filter(a=>a.provider_name===name);
|
||
|
||
if(!accounts.length){
|
||
// Fallback: query DB directly through router
|
||
div.innerHTML=`<p style="font-size:11px;color:#64748b">Utilise le scan DB pour charger...</p>`;
|
||
const r2=await fetch(ROUTER+'?action=providers');
|
||
const d2=await r2.json();
|
||
div.innerHTML=`<p style="font-size:11px;color:#64748b">${(d2.multi_accounts||[]).find(m=>m.provider_name===name)?.total_accounts||0} comptes trouvés. Utilise l'API factory pour les gérer.</p>`;
|
||
return;
|
||
}
|
||
|
||
let h=`<table class="ptable"><thead><tr>
|
||
<th>#</th><th>Persona</th><th>Email</th><th>Status</th><th>API Key</th><th>Actions</th>
|
||
</tr></thead><tbody>`;
|
||
accounts.forEach(a=>{
|
||
const hasKey=a.has_key||false;
|
||
const keyVal=hasKey?'••••••••••':'';
|
||
h+=`<tr>
|
||
<td>${a.id||'-'}</td>
|
||
<td>${a.persona_name||a.account_email||'-'}</td>
|
||
<td style="font-size:10px">${a.account_email||'-'}</td>
|
||
<td><span class="badge ${hasKey?'ok':'pend'}">${a.status||'pending'}</span></td>
|
||
<td><input id="pk-${a.id}" placeholder="API key..." value="${keyVal}"></td>
|
||
<td><button class="btn btn-s" onclick="savePersonaKey(${a.id},'${name}')">💾</button></td>
|
||
</tr>`;
|
||
});
|
||
h+='</tbody></table>';
|
||
div.innerHTML=h;
|
||
}catch(e){div.innerHTML=`<p style="color:var(--rd);font-size:11px">❌ ${e.message}</p>`;}
|
||
}
|
||
|
||
async function savePersonaKey(id,provider){
|
||
const key=document.getElementById('pk-'+id)?.value?.trim();
|
||
if(!key||key==='••••••••••'){alert('Colle une vraie clé API');return;}
|
||
try{
|
||
await fetch(FACTORY,{method:'POST',headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify({action:'update_key',account_id:id,api_key:key})});
|
||
alert('✅ Key saved #'+id);
|
||
loadPersonas(provider);
|
||
}catch(e){alert('❌ '+e.message);}
|
||
}
|
||
|
||
async function testAllKeys(name){
|
||
const div=document.getElementById('mainres-'+name);
|
||
div.innerHTML='<div class="res" style="color:var(--bl)">⏳ Testing all keys for '+name+'...</div>';
|
||
try{
|
||
const r=await fetch(ROUTER+'?action=rotate_test&provider='+encodeURIComponent(name));
|
||
const d=await r.json();
|
||
const results=d.results||[];
|
||
let h=results.map(r=>`<span class="badge ${r.ok?'ok':'err'}">${r.provider}${r.account_id?'#'+r.account_id:''}: ${r.ok?r.latency+'ms':'❌'}</span>`).join(' ');
|
||
div.innerHTML=`<div class="res ${results.some(r=>r.ok)?'ok':'fail'}">${h||'No results'}</div>`;
|
||
}catch(e){div.innerHTML=`<div class="res fail">❌ ${e.message}</div>`;}
|
||
}
|
||
|
||
loadAll();
|
||
</script>
|
||
<script src="arsenal-common.js?v1770778169">
|
||
<?php include("/opt/wevads-arsenal/public/universal-drill.html"); ?>
|
||
</body>
|
||
</html>
|
||
</script>
|