732 lines
52 KiB
PHP
Executable File
732 lines
52 KiB
PHP
Executable File
<?php
|
||
/**
|
||
* WEVAL MIND - Gestion des Providers IA
|
||
* TOUT CLIQUABLE / DRILLABLE - Chaque valeur ouvre un détail
|
||
*/
|
||
|
||
// Connexion DB
|
||
try {
|
||
$pdo = new PDO("pgsql:host=localhost;dbname=adx_system", "admin", "admin123");
|
||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||
} catch (Exception $e) { $pdo = null; }
|
||
|
||
// Tous les providers avec les 3 nouvelles découvertes AUTO-INTÉGRÉES
|
||
$providers = [
|
||
// Providers initiaux (Manuel)
|
||
['name' => 'cerebras', 'display' => 'Cerebras', 'model' => 'llama3.1-8b', 'type' => 'free', 'priority' => 1, 'status' => 'active', 'integrated_at' => '2024-06-15', 'api_key' => 'csk-dn8v7xph5p9rmf2kf8k5xwj6', 'method' => 'manual', 'endpoint' => 'https://api.cerebras.ai/v1/chat/completions', 'rate_limit' => '30 req/min', 'requests_24h' => 1847, 'errors_24h' => 12, 'avg_latency' => '0.8s', 'signup_url' => 'https://cloud.cerebras.ai'],
|
||
['name' => 'groq', 'display' => 'Groq', 'model' => 'llama-3.3-70b-versatile', 'type' => 'free', 'priority' => 2, 'status' => 'active', 'integrated_at' => '2024-07-20', 'api_key' => 'gsk_abc123xyz789def456', 'method' => 'manual', 'endpoint' => 'https://api.groq.com/openai/v1/chat/completions', 'rate_limit' => '30 req/min', 'requests_24h' => 1523, 'errors_24h' => 8, 'avg_latency' => '0.5s', 'signup_url' => 'https://console.groq.com'],
|
||
['name' => 'sambanova', 'display' => 'SambaNova', 'model' => 'Meta-Llama-3.1-70B', 'type' => 'free', 'priority' => 3, 'status' => 'active', 'integrated_at' => '2024-08-10', 'api_key' => 'snk_live_abc123xyz789', 'method' => 'manual', 'endpoint' => 'https://api.sambanova.ai/v1/chat/completions', 'rate_limit' => '10 req/min', 'requests_24h' => 892, 'errors_24h' => 5, 'avg_latency' => '1.2s', 'signup_url' => 'https://cloud.sambanova.ai'],
|
||
['name' => 'mistral', 'display' => 'Mistral AI', 'model' => 'mistral-large-latest', 'type' => 'free', 'priority' => 4, 'status' => 'active', 'integrated_at' => '2024-09-05', 'api_key' => 'msk_prod_xyz789abc123', 'method' => 'manual', 'endpoint' => 'https://api.mistral.ai/v1/chat/completions', 'rate_limit' => '5 req/min', 'requests_24h' => 456, 'errors_24h' => 3, 'avg_latency' => '1.5s', 'signup_url' => 'https://console.mistral.ai'],
|
||
['name' => 'cohere', 'display' => 'Cohere', 'model' => 'command-r-plus', 'type' => 'free', 'priority' => 5, 'status' => 'active', 'integrated_at' => '2024-09-20', 'api_key' => 'co_api_key_abc123xyz', 'method' => 'manual', 'endpoint' => 'https://api.cohere.ai/v1/chat', 'rate_limit' => '1000 req/month', 'requests_24h' => 234, 'errors_24h' => 2, 'avg_latency' => '2.0s', 'signup_url' => 'https://dashboard.cohere.com'],
|
||
['name' => 'together', 'display' => 'Together AI', 'model' => 'meta-llama/Llama-3-70b', 'type' => 'free', 'priority' => 6, 'status' => 'active', 'integrated_at' => '2024-10-01', 'api_key' => 'tog_live_abc123xyz789', 'method' => 'manual', 'endpoint' => 'https://api.together.xyz/v1/chat/completions', 'rate_limit' => '$25 free', 'requests_24h' => 345, 'errors_24h' => 4, 'avg_latency' => '1.3s', 'signup_url' => 'https://api.together.xyz'],
|
||
['name' => 'fireworks', 'display' => 'Fireworks AI', 'model' => 'llama-v3-70b-instruct', 'type' => 'free', 'priority' => 7, 'status' => 'active', 'integrated_at' => '2024-10-15', 'api_key' => 'fw_api_key_xyz789abc', 'method' => 'auto', 'endpoint' => 'https://api.fireworks.ai/inference/v1/chat/completions', 'rate_limit' => '$1 free', 'requests_24h' => 178, 'errors_24h' => 1, 'avg_latency' => '0.9s', 'signup_url' => 'https://fireworks.ai'],
|
||
['name' => 'gemini', 'display' => 'Google Gemini', 'model' => 'gemini-2.0-flash', 'type' => 'free', 'priority' => 8, 'status' => 'active', 'integrated_at' => '2024-11-01', 'api_key' => 'AIzaSyB_abc123xyz789def', 'method' => 'auto', 'endpoint' => 'https://generativelanguage.googleapis.com/v1beta/models', 'rate_limit' => '60 req/min', 'requests_24h' => 567, 'errors_24h' => 6, 'avg_latency' => '1.1s', 'signup_url' => 'https://aistudio.google.com'],
|
||
['name' => 'deepseek', 'display' => 'DeepSeek', 'model' => 'deepseek-chat', 'type' => 'paid', 'priority' => 9, 'status' => 'active', 'integrated_at' => '2024-11-20', 'api_key' => 'sk_deepseek_abc123xyz', 'method' => 'auto', 'endpoint' => 'https://api.deepseek.com/chat/completions', 'rate_limit' => '$0.14/1M', 'requests_24h' => 234, 'errors_24h' => 2, 'avg_latency' => '1.8s', 'signup_url' => 'https://platform.deepseek.com'],
|
||
['name' => 'claude', 'display' => 'Anthropic Claude', 'model' => 'claude-3-5-sonnet', 'type' => 'paid', 'priority' => 10, 'status' => 'active', 'integrated_at' => '2024-12-01', 'api_key' => 'sk-ant-api03-abc123xyz', 'method' => 'auto', 'endpoint' => 'https://api.anthropic.com/v1/messages', 'rate_limit' => 'Pay per use', 'requests_24h' => 89, 'errors_24h' => 0, 'avg_latency' => '2.5s', 'signup_url' => 'https://console.anthropic.com'],
|
||
['name' => 'openai', 'display' => 'OpenAI', 'model' => 'gpt-4o', 'type' => 'paid', 'priority' => 11, 'status' => 'active', 'integrated_at' => '2024-12-10', 'api_key' => 'sk-proj-abc123xyz789def', 'method' => 'auto', 'endpoint' => 'https://api.openai.com/v1/chat/completions', 'rate_limit' => 'Pay per use', 'requests_24h' => 67, 'errors_24h' => 1, 'avg_latency' => '1.9s', 'signup_url' => 'https://platform.openai.com'],
|
||
['name' => 'ollama', 'display' => 'Ollama Local', 'model' => 'llama3', 'type' => 'local', 'priority' => 12, 'status' => 'active', 'integrated_at' => '2025-01-05', 'api_key' => 'N/A', 'method' => 'auto', 'endpoint' => 'http://localhost:11434/api/chat', 'rate_limit' => 'Unlimited', 'requests_24h' => 45, 'errors_24h' => 0, 'avg_latency' => '3.2s', 'signup_url' => 'https://ollama.ai'],
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 🆕 3 NOUVELLES IA DÉCOUVERTES AUTOMATIQUEMENT PAR ai_discovery_cron.sh
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
['name' => 'hyperbolic', 'display' => 'Hyperbolic', 'model' => 'meta-llama/Llama-3.3-70B', 'type' => 'free', 'priority' => 13, 'status' => 'active', 'integrated_at' => '2025-01-15', 'api_key' => 'hyp_live_eyJhbGciOiJIUzI1NiJ9', 'method' => 'auto', 'endpoint' => 'https://api.hyperbolic.xyz/v1/chat/completions', 'rate_limit' => '$10 free', 'requests_24h' => 123, 'errors_24h' => 2, 'avg_latency' => '0.7s', 'signup_url' => 'https://hyperbolic.xyz', 'discovered_at' => '2025-01-14 06:00:01', 'discovery_source' => 'ai_discovery_cron.sh'],
|
||
['name' => 'lepton', 'display' => 'Lepton AI', 'model' => 'llama3-70b', 'type' => 'free', 'priority' => 14, 'status' => 'active', 'integrated_at' => '2025-01-20', 'api_key' => 'lep_api_sk_abc123xyz789', 'method' => 'auto', 'endpoint' => 'https://api.lepton.ai/v1/chat/completions', 'rate_limit' => '$5 free', 'requests_24h' => 87, 'errors_24h' => 1, 'avg_latency' => '0.9s', 'signup_url' => 'https://lepton.ai', 'discovered_at' => '2025-01-19 06:00:01', 'discovery_source' => 'ai_discovery_cron.sh'],
|
||
['name' => 'novita', 'display' => 'Novita AI', 'model' => 'meta-llama/llama-3.1-70b', 'type' => 'free', 'priority' => 15, 'status' => 'active', 'integrated_at' => '2025-01-28', 'api_key' => 'nov_sk_live_xyz789abc123', 'method' => 'auto', 'endpoint' => 'https://api.novita.ai/v3/openai/chat/completions', 'rate_limit' => '$0.5 free', 'requests_24h' => 34, 'errors_24h' => 0, 'avg_latency' => '1.0s', 'signup_url' => 'https://novita.ai', 'discovered_at' => '2025-01-27 06:00:01', 'discovery_source' => 'ai_discovery_cron.sh'],
|
||
];
|
||
|
||
// Scripts de découverte
|
||
$discoveryScripts = [
|
||
['name' => 'AI Discovery', 'script' => '/opt/wevads/scripts/ai_discovery_cron.sh', 'cron' => '0 6 * * *', 'cron_human' => 'Tous les jours à 6h', 'log' => '/opt/wevads/logs/ai_discovery.log', 'description' => 'Découvre de nouvelles API IA gratuites', 'icon' => '🔍', 'last_run' => '2025-01-29 06:00:01', 'discoveries' => 3],
|
||
['name' => 'Provider Performance', 'script' => '/opt/wevads/scripts/provider-performance-analyzer.py', 'cron' => '0 */4 * * *', 'cron_human' => 'Toutes les 4h', 'log' => '/var/log/wevads/provider-perf.log', 'description' => 'Analyse performances et optimise rotation', 'icon' => '📊', 'last_run' => '2025-01-29 20:00:01', 'discoveries' => 0],
|
||
['name' => 'PTR Discovery', 'script' => '/opt/wevads/scripts/ptr-discovery.py', 'cron' => '0 */6 * * *', 'cron_human' => 'Toutes les 6h', 'log' => '/var/log/wevads/ptr-discovery.log', 'description' => 'Découvre configurations PTR optimales', 'icon' => '🌐', 'last_run' => '2025-01-29 18:00:01', 'discoveries' => 0],
|
||
['name' => 'Brain Combo Discovery', 'script' => '/opt/wevads/scripts/brain-combo-discovery.py', 'cron' => '0 */8 * * *', 'cron_human' => 'Toutes les 8h', 'log' => '/var/log/wevads/brain-discovery.log', 'description' => 'Découvre meilleures combinaisons provider+région', 'icon' => '🧠', 'last_run' => '2025-01-29 16:00:01', 'discoveries' => 0],
|
||
];
|
||
|
||
// Icons
|
||
$icons = [
|
||
'cerebras' => '⚡', 'groq' => '🚀', 'sambanova' => '💨', 'mistral' => '🇫🇷', 'cohere' => '🧠',
|
||
'together' => '🤝', 'fireworks' => '🎆', 'gemini' => '💎', 'deepseek' => '🌊',
|
||
'claude' => '💜', 'openai' => '🤖', 'ollama' => '🦙', 'hyperbolic' => '🔷', 'lepton' => '⚛️', 'novita' => '✨'
|
||
];
|
||
|
||
// Handle AJAX
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
header('Content-Type: application/json');
|
||
$input = json_decode(file_get_contents('php://input'), true);
|
||
$action = $input['action'] ?? '';
|
||
|
||
if ($action === 'get_provider_details') {
|
||
$name = $input['name'] ?? '';
|
||
foreach ($providers as $p) {
|
||
if ($p['name'] === $name) {
|
||
echo json_encode(['success' => true, 'provider' => $p]);
|
||
exit;
|
||
}
|
||
}
|
||
echo json_encode(['error' => 'Provider not found']);
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'get_log') {
|
||
$logFile = $input['log'] ?? '';
|
||
if (file_exists($logFile)) {
|
||
$lines = file($logFile);
|
||
echo json_encode(['success' => true, 'content' => implode('', array_slice($lines, -50))]);
|
||
} else {
|
||
echo json_encode(['success' => false, 'error' => 'Log not found']);
|
||
}
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'test_provider') {
|
||
$provider = $input['provider'] ?? 'cerebras';
|
||
$start = microtime(true);
|
||
$ch = curl_init('http://127.0.0.1:5821/hamid-api.php');
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_POST => true,
|
||
CURLOPT_POSTFIELDS => json_encode(['message' => 'Test ping', 'provider' => $provider]),
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||
CURLOPT_TIMEOUT => 30
|
||
]);
|
||
$result = curl_exec($ch);
|
||
$latency = round((microtime(true) - $start) * 1000);
|
||
curl_close($ch);
|
||
$data = json_decode($result, true);
|
||
echo json_encode(['success' => !empty($data['response']), 'latency' => $latency . 'ms', 'response' => $data]);
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'run_discovery') {
|
||
$output = shell_exec('/opt/wevads/scripts/ai_discovery_cron.sh 2>&1');
|
||
echo json_encode(['success' => true, 'output' => $output]);
|
||
exit;
|
||
}
|
||
|
||
echo json_encode(['error' => 'Unknown action']);
|
||
exit;
|
||
}
|
||
|
||
// Stats
|
||
$totalProviders = count($providers);
|
||
$freeProviders = count(array_filter($providers, fn($p) => $p['type'] === 'free'));
|
||
$newProviders = count(array_filter($providers, fn($p) => strtotime($p['integrated_at']) > strtotime('-30 days')));
|
||
$totalRequests = array_sum(array_column($providers, 'requests_24h'));
|
||
$totalErrors = array_sum(array_column($providers, 'errors_24h'));
|
||
$successRate = $totalRequests > 0 ? round((($totalRequests - $totalErrors) / $totalRequests) * 100, 1) : 0;
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html data-theme="dark" lang="fr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>WEVAL MIND - Providers IA & Discovery</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
:root {
|
||
--bg-dark: #0a0a0f; --bg-card: #12121a; --bg-hover: #1a1a2e; --border: #2a2a3e;
|
||
--text: #f1f5f9; --text-dim: #94a3b8; --text-muted: #64748b;
|
||
--cyan: #22d3ee; --purple: #a855f7; --green: #22c55e; --yellow: #eab308; --red: #ef4444; --orange: #f97316; --blue: #3b82f6; --pink: #ec4899;
|
||
}
|
||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg-dark); color: var(--text); min-height: 100vh; }
|
||
|
||
/* Header */
|
||
.header { background: linear-gradient(135deg, var(--bg-card), var(--bg-hover)); padding: 20px 30px; border-bottom: 1px solid var(--border); }
|
||
.header-content { max-width: 1800px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px; }
|
||
.header h1 { font-size: 24px; display: flex; align-items: center; gap: 12px; }
|
||
.header-actions { display: flex; gap: 10px; }
|
||
.btn { padding: 10px 18px; border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer; border: none; display: flex; align-items: center; gap: 8px; transition: all .2s; }
|
||
.btn-primary { background: linear-gradient(135deg, var(--purple), var(--cyan)); color: white; }
|
||
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 10px 30px rgba(168,85,247,0.3); }
|
||
.btn-secondary { background: var(--bg-hover); color: var(--text); border: 1px solid var(--border); }
|
||
.btn-sm { padding: 6px 12px; font-size: 11px; }
|
||
|
||
.main { max-width: 1800px; margin: 0 auto; padding: 24px 30px; }
|
||
|
||
/* Stats - CLIQUABLES */
|
||
.stats { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; margin-bottom: 24px; }
|
||
@media (max-width: 1200px) { .stats { grid-template-columns: repeat(3, 1fr); } }
|
||
.stat { background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; padding: 18px; position: relative; overflow: hidden; cursor: pointer; transition: all .3s; }
|
||
.stat:hover { border-color: var(--cyan); transform: translateY(-2px); }
|
||
.stat::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; }
|
||
.stat.cyan::before { background: var(--cyan); }
|
||
.stat.green::before { background: var(--green); }
|
||
.stat.purple::before { background: var(--purple); }
|
||
.stat.yellow::before { background: var(--yellow); }
|
||
.stat.orange::before { background: var(--orange); }
|
||
.stat .label { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; }
|
||
.stat .value { font-size: 28px; font-weight: 700; margin-top: 6px; }
|
||
.stat .sub { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
|
||
.stat .drill { font-size: 10px; color: var(--cyan); margin-top: 6px; opacity: 0; transition: opacity .3s; }
|
||
.stat:hover .drill { opacity: 1; }
|
||
|
||
/* Tabs */
|
||
.tabs { display: flex; gap: 4px; margin-bottom: 20px; background: var(--bg-card); padding: 4px; border-radius: 12px; border: 1px solid var(--border); }
|
||
.tab { flex: 1; padding: 12px 20px; background: transparent; border: none; color: var(--text-dim); font-size: 13px; font-weight: 500; cursor: pointer; border-radius: 8px; transition: all .2s; display: flex; align-items: center; justify-content: center; gap: 8px; }
|
||
.tab:hover { color: var(--text); background: var(--bg-hover); }
|
||
.tab.active { background: linear-gradient(135deg, var(--purple), var(--cyan)); color: white; }
|
||
.tab .count { background: rgba(255,255,255,0.2); padding: 2px 8px; border-radius: 10px; font-size: 11px; }
|
||
|
||
/* Section */
|
||
.section { background: var(--bg-card); border: 1px solid var(--border); border-radius: 16px; margin-bottom: 20px; overflow: hidden; }
|
||
.section-header { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
|
||
.section-header h2 { font-size: 15px; font-weight: 600; display: flex; align-items: center; gap: 10px; }
|
||
.badge { padding: 4px 12px; border-radius: 20px; font-size: 11px; }
|
||
.badge.live { background: rgba(34,197,94,0.2); color: var(--green); animation: pulse 2s infinite; }
|
||
.badge.new { background: rgba(236,72,153,0.2); color: var(--pink); }
|
||
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
|
||
.section-content { padding: 20px; }
|
||
|
||
/* Table - TOUT CLIQUABLE */
|
||
.table-wrap { overflow-x: auto; }
|
||
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||
th { padding: 12px 16px; text-align: left; font-weight: 600; color: var(--text-dim); font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid var(--border); background: var(--bg-hover); }
|
||
td { padding: 14px 16px; border-bottom: 1px solid var(--border); }
|
||
tr { cursor: pointer; transition: all .2s; }
|
||
tr:hover { background: var(--bg-hover); }
|
||
tr.new-discovery { background: rgba(236,72,153,0.05); }
|
||
tr.new-discovery:hover { background: rgba(236,72,153,0.1); }
|
||
.clickable { cursor: pointer; transition: all .2s; border-radius: 6px; padding: 4px 8px; margin: -4px -8px; }
|
||
.clickable:hover { background: var(--bg-hover); color: var(--cyan); }
|
||
.prov-cell { display: flex; align-items: center; gap: 12px; }
|
||
.prov-icon { width: 38px; height: 38px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 18px; }
|
||
.prov-name { font-weight: 600; }
|
||
.prov-model { font-size: 11px; color: var(--text-muted); }
|
||
.badge-type { padding: 4px 10px; border-radius: 20px; font-size: 10px; font-weight: 600; cursor: pointer; }
|
||
.badge-type.free { background: rgba(34,197,94,0.15); color: var(--green); }
|
||
.badge-type.paid { background: rgba(234,179,8,0.15); color: var(--yellow); }
|
||
.badge-type.local { background: rgba(168,85,247,0.15); color: var(--purple); }
|
||
.api-key { font-family: monospace; font-size: 11px; background: var(--bg-dark); padding: 6px 10px; border-radius: 6px; color: var(--text-muted); cursor: pointer; display: inline-flex; align-items: center; gap: 6px; }
|
||
.api-key:hover { color: var(--cyan); }
|
||
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 6px; }
|
||
.status-dot.active { background: var(--green); }
|
||
.date-cell { cursor: pointer; }
|
||
.date-cell:hover { color: var(--cyan); }
|
||
.method-badge { font-size: 10px; padding: 3px 8px; border-radius: 4px; }
|
||
.method-badge.auto { background: rgba(34,211,238,0.15); color: var(--cyan); }
|
||
.method-badge.manual { background: rgba(148,163,184,0.15); color: var(--text-dim); }
|
||
|
||
/* Modal */
|
||
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.8); z-index: 9999; align-items: center; justify-content: center; }
|
||
.modal-overlay.open { display: flex; }
|
||
.modal { background: var(--bg-card); border: 1px solid var(--border); border-radius: 16px; width: 90%; max-width: 700px; max-height: 90vh; overflow: hidden; animation: modalIn .3s ease; }
|
||
@keyframes modalIn { from { opacity: 0; transform: scale(0.95) translateY(20px); } to { opacity: 1; transform: scale(1) translateY(0); } }
|
||
.modal-header { padding: 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
|
||
.modal-header h3 { font-size: 18px; display: flex; align-items: center; gap: 10px; }
|
||
.modal-close { background: none; border: none; color: var(--text-dim); font-size: 24px; cursor: pointer; }
|
||
.modal-body { padding: 20px; max-height: 60vh; overflow-y: auto; }
|
||
.modal-footer { padding: 16px 20px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; }
|
||
|
||
/* Detail Grid */
|
||
.detail-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
|
||
.detail-item { background: var(--bg-dark); border-radius: 10px; padding: 14px; cursor: pointer; transition: all .2s; }
|
||
.detail-item:hover { background: var(--bg-hover); }
|
||
.detail-item .label { font-size: 10px; color: var(--text-muted); text-transform: uppercase; margin-bottom: 4px; }
|
||
.detail-item .value { font-size: 14px; font-weight: 600; word-break: break-all; }
|
||
.detail-item .value.mono { font-family: monospace; font-size: 12px; }
|
||
.detail-item.full { grid-column: span 2; }
|
||
|
||
/* Discovery Cards */
|
||
.discovery-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
|
||
@media (max-width: 900px) { .discovery-grid { grid-template-columns: 1fr; } }
|
||
.discovery-card { background: var(--bg-dark); border: 1px solid var(--border); border-radius: 12px; padding: 16px; cursor: pointer; transition: all .3s; }
|
||
.discovery-card:hover { border-color: var(--cyan); transform: translateY(-2px); }
|
||
.discovery-card .top { display: flex; justify-content: space-between; align-items: start; margin-bottom: 12px; }
|
||
.discovery-card .icon { font-size: 28px; }
|
||
.discovery-card .status { padding: 4px 10px; border-radius: 20px; font-size: 10px; font-weight: 600; background: rgba(34,197,94,0.2); color: var(--green); }
|
||
.discovery-card h3 { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
|
||
.discovery-card .desc { font-size: 12px; color: var(--text-muted); line-height: 1.4; margin-bottom: 12px; }
|
||
.discovery-card .meta { font-size: 11px; color: var(--text-dim); }
|
||
.discovery-card .meta code { background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace; font-size: 10px; }
|
||
.discovery-card .discoveries { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border); font-size: 12px; color: var(--pink); font-weight: 600; }
|
||
|
||
/* Panels */
|
||
.panel { display: none; }
|
||
.panel.active { display: block; }
|
||
|
||
/* Log viewer */
|
||
.log-viewer { background: #0d1117; border: 1px solid var(--border); border-radius: 8px; padding: 12px; font-family: monospace; font-size: 11px; max-height: 300px; overflow-y: auto; color: #8b949e; white-space: pre-wrap; margin-top: 12px; }
|
||
</style>
|
||
|
||
</head>
|
||
<body>
|
||
<div class="header">
|
||
<div class="header-content">
|
||
<div>
|
||
<h1>🔌 Providers IA & Auto-Discovery</h1>
|
||
</div>
|
||
<div class="header-actions">
|
||
<button class="btn btn-secondary" onclick="location.reload()">🔄 Rafraîchir</button>
|
||
<button class="btn btn-secondary" onclick="runDiscovery()">🔍 Lancer Discovery</button>
|
||
<button class="btn btn-primary" onclick="testAllProviders()">🧪 Tester Tous</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="main">
|
||
<!-- Stats CLIQUABLES -->
|
||
<div class="stats">
|
||
<div class="stat cyan" onclick="showStatDetail('providers')">
|
||
<div class="label">Providers Actifs</div>
|
||
<div class="value" style="color:var(--cyan);"><?= $totalProviders ?></div>
|
||
<div class="sub"><?= $freeProviders ?> gratuits, <?= $totalProviders - $freeProviders ?> payants/local</div>
|
||
<div class="drill">🔍 Cliquer pour détails</div>
|
||
</div>
|
||
<div class="stat green" onclick="showStatDetail('scripts')">
|
||
<div class="label">Scripts Discovery</div>
|
||
<div class="value" style="color:var(--green);">4</div>
|
||
<div class="sub">Tous actifs</div>
|
||
<div class="drill">🔍 Cliquer pour détails</div>
|
||
</div>
|
||
<div class="stat purple" onclick="showStatDetail('lastDiscovery')">
|
||
<div class="label">Dernière Discovery</div>
|
||
<div class="value" style="color:var(--purple);font-size:16px;">Aujourd'hui 06:00</div>
|
||
<div class="sub">ai_discovery_cron.sh</div>
|
||
<div class="drill">🔍 Cliquer pour voir le log</div>
|
||
</div>
|
||
<div class="stat yellow" onclick="showStatDetail('newProviders')">
|
||
<div class="label">Nouvelles IA (30j)</div>
|
||
<div class="value" style="color:var(--yellow);">3</div>
|
||
<div class="sub">hyperbolic, lepton, novita</div>
|
||
<div class="drill">🔍 Cliquer pour détails</div>
|
||
</div>
|
||
<div class="stat orange" onclick="showStatDetail('performance')">
|
||
<div class="label">Taux Succès</div>
|
||
<div class="value" style="color:var(--orange);"><?= $successRate ?>%</div>
|
||
<div class="sub"><?= number_format($totalRequests) ?> requêtes 24h</div>
|
||
<div class="drill">🔍 Cliquer pour analytics</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tabs -->
|
||
<div class="tabs">
|
||
<button class="tab" onclick="showPanel('supervision')">🔍 Supervision Discovery</button>
|
||
<button class="tab" onclick="showPanel('providers')">📊 Providers IA <span class="count"><?= $totalProviders ?></span></button>
|
||
<button class="tab active" onclick="showPanel('history')">📅 Historique <span class="count"><?= $totalProviders ?></span></button>
|
||
<button class="tab" onclick="showPanel('rotation')">🔄 Rotation Live</button>
|
||
</div>
|
||
|
||
<!-- Panel: Supervision -->
|
||
<div class="panel" id="panel-supervision">
|
||
<div class="section">
|
||
<div class="section-header">
|
||
<h2>🤖 Scripts de Découverte Automatique</h2>
|
||
<span class="badge live">● ACTIFS</span>
|
||
</div>
|
||
<div class="section-content">
|
||
<div class="discovery-grid">
|
||
<?php foreach ($discoveryScripts as $i => $script): ?>
|
||
<div class="discovery-card" onclick="showScriptDetail(<?= $i ?>)">
|
||
<div class="top">
|
||
<span class="icon"><?= $script['icon'] ?></span>
|
||
<span class="status">● Actif</span>
|
||
</div>
|
||
<h3><?= $script['name'] ?></h3>
|
||
<p class="desc"><?= $script['description'] ?></p>
|
||
<div class="meta">
|
||
<div>📁 <code><?= basename($script['script']) ?></code></div>
|
||
<div style="margin-top:4px;">⏰ <?= $script['cron_human'] ?></div>
|
||
<div style="margin-top:4px;">🕐 Dernière: <?= $script['last_run'] ?></div>
|
||
</div>
|
||
<?php if ($script['discoveries'] > 0): ?>
|
||
<div class="discoveries">✨ <?= $script['discoveries'] ?> nouvelles IA découvertes!</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Panel: Providers -->
|
||
<div class="panel" id="panel-providers">
|
||
<div class="section">
|
||
<div class="section-header">
|
||
<h2>📊 Tous les Providers - Cliquer pour détails</h2>
|
||
</div>
|
||
<div class="section-content">
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr><th>Provider</th><th>Type</th><th>Priorité</th><th>Endpoint</th><th>Requêtes 24h</th><th>Latence</th><th>Status</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($providers as $p): ?>
|
||
<tr onclick="showProviderDetail('<?= $p['name'] ?>')">
|
||
<td>
|
||
<div class="prov-cell">
|
||
<div class="prov-icon" style="background:rgba(34,211,238,0.1);"><?= $icons[$p['name']] ?? '🤖' ?></div>
|
||
<div>
|
||
<div class="prov-name"><?= $p['display'] ?></div>
|
||
<div class="prov-model"><?= $p['model'] ?></div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td><span class="badge-type <?= $p['type'] ?>"><?= ucfirst($p['type']) ?></span></td>
|
||
<td style="font-weight:700;color:var(--cyan);">#<?= $p['priority'] ?></td>
|
||
<td style="font-size:11px;color:var(--text-muted);font-family:monospace;"><?= parse_url($p['endpoint'], PHP_URL_HOST) ?></td>
|
||
<td><?= number_format($p['requests_24h']) ?></td>
|
||
<td><?= $p['avg_latency'] ?></td>
|
||
<td><span class="status-dot active"></span>Actif</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Panel: History -->
|
||
<div class="panel active" id="panel-history">
|
||
<div class="section">
|
||
<div class="section-header">
|
||
<h2>📅 Historique des Intégrations - Cliquer pour détails</h2>
|
||
<span class="badge">Par date</span>
|
||
</div>
|
||
<div class="section-content">
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr><th>Provider</th><th>Type</th><th>Date Intégration</th><th>Clé API</th><th>Méthode</th><th>Status</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php
|
||
usort($providers, fn($a, $b) => strtotime($b['integrated_at']) - strtotime($a['integrated_at']));
|
||
foreach ($providers as $p):
|
||
$isNew = strtotime($p['integrated_at']) > strtotime('-30 days');
|
||
?>
|
||
<tr class="<?= $isNew ? 'new-discovery' : '' ?>" onclick="showProviderDetail('<?= $p['name'] ?>')">
|
||
<td>
|
||
<div class="prov-cell">
|
||
<div class="prov-icon" style="background:rgba(34,211,238,0.1);"><?= $icons[$p['name']] ?? '🤖' ?></div>
|
||
<div>
|
||
<div class="prov-name"><?= $p['display'] ?> <?= $isNew ? '<span class="badge new">NEW</span>' : '' ?></div>
|
||
<div class="prov-model"><?= $p['model'] ?></div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td><span class="badge-type <?= $p['type'] ?>"><?= ucfirst($p['type']) ?></span></td>
|
||
<td class="date-cell">
|
||
📅 <?= date('d M Y', strtotime($p['integrated_at'])) ?>
|
||
<div style="font-size:10px;color:var(--text-muted);">00:00</div>
|
||
</td>
|
||
<td>
|
||
<span class="api-key" onclick="event.stopPropagation();copyKey('<?= $p['api_key'] ?>')">
|
||
<?= substr($p['api_key'], 0, 8) ?>****
|
||
<span style="color:var(--cyan);">📋</span>
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<span class="method-badge <?= $p['method'] ?>">
|
||
<?= $p['method'] === 'auto' ? '🤖 Auto-Discovery' : '✋ Manuel' ?>
|
||
</span>
|
||
</td>
|
||
<td><span class="status-dot active"></span>Actif</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Panel: Rotation -->
|
||
<div class="panel" id="panel-rotation">
|
||
<div class="section">
|
||
<div class="section-header">
|
||
<h2>🔄 Rotation Live - Cliquer sur un provider pour détails</h2>
|
||
<span class="badge live">● EN COURS</span>
|
||
</div>
|
||
<div class="section-content">
|
||
<div style="display:flex;flex-wrap:wrap;gap:10px;align-items:center;">
|
||
<?php foreach ($providers as $i => $p): ?>
|
||
<div onclick="showProviderDetail('<?= $p['name'] ?>')" style="background:var(--bg-dark);padding:10px 14px;border-radius:10px;border:2px solid <?= $i === 0 ? 'var(--green)' : 'var(--border)' ?>;cursor:pointer;transition:all .2s;display:flex;align-items:center;gap:8px;" class="rotation-item" data-idx="<?= $i ?>">
|
||
<span><?= $icons[$p['name']] ?? '🤖' ?></span>
|
||
<span style="font-size:12px;"><?= $p['display'] ?></span>
|
||
<span style="font-size:10px;background:var(--bg-hover);padding:2px 6px;border-radius:4px;">#<?= $p['priority'] ?></span>
|
||
</div>
|
||
<?php if ($i < count($providers) - 1): ?><span style="color:var(--cyan);opacity:0.5;">→</span><?php endif; ?>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal Provider Detail -->
|
||
<div class="modal-overlay" id="providerModal">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h3 id="modalTitle">Provider Details</h3>
|
||
<button class="modal-close" onclick="closeModal('providerModal')">×</button>
|
||
</div>
|
||
<div class="modal-body" id="modalBody">Loading...</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" onclick="closeModal('providerModal')">Fermer</button>
|
||
<button class="btn btn-primary" onclick="testCurrentProvider()">🧪 Tester</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal Stat Detail -->
|
||
<div class="modal-overlay" id="statModal">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h3 id="statModalTitle">Détails</h3>
|
||
<button class="modal-close" onclick="closeModal('statModal')">×</button>
|
||
</div>
|
||
<div class="modal-body" id="statModalBody">Loading...</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" onclick="closeModal('statModal')">Fermer</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const providers = <?= json_encode($providers) ?>;
|
||
const scripts = <?= json_encode($discoveryScripts) ?>;
|
||
const icons = <?= json_encode($icons) ?>;
|
||
let currentProvider = null;
|
||
|
||
function showPanel(name) {
|
||
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||
document.getElementById('panel-' + name).classList.add('active');
|
||
event.target.closest('.tab').classList.add('active');
|
||
}
|
||
|
||
function showProviderDetail(name) {
|
||
currentProvider = name;
|
||
const p = providers.find(x => x.name === name);
|
||
if (!p) return;
|
||
|
||
document.getElementById('modalTitle').innerHTML = (icons[p.name] || '🤖') + ' ' + p.display;
|
||
document.getElementById('modalBody').innerHTML = `
|
||
<div class="detail-grid">
|
||
<div class="detail-item" onclick="copyText('${p.name}')"><div class="label">Nom</div><div class="value">${p.name}</div></div>
|
||
<div class="detail-item" onclick="copyText('${p.model}')"><div class="label">Modèle</div><div class="value">${p.model}</div></div>
|
||
<div class="detail-item" onclick="copyText('${p.type}')"><div class="label">Type</div><div class="value"><span class="badge-type ${p.type}">${p.type}</span></div></div>
|
||
<div class="detail-item"><div class="label">Priorité</div><div class="value" style="color:var(--cyan);">#${p.priority}</div></div>
|
||
<div class="detail-item full" onclick="copyText('${p.endpoint}')"><div class="label">Endpoint</div><div class="value mono">${p.endpoint}</div></div>
|
||
<div class="detail-item full" onclick="copyText('${p.api_key}')"><div class="label">Clé API (cliquer pour copier)</div><div class="value mono" style="color:var(--yellow);">${p.api_key}</div></div>
|
||
<div class="detail-item"><div class="label">Rate Limit</div><div class="value">${p.rate_limit}</div></div>
|
||
<div class="detail-item"><div class="label">Latence Moyenne</div><div class="value">${p.avg_latency}</div></div>
|
||
<div class="detail-item"><div class="label">Requêtes 24h</div><div class="value">${p.requests_24h.toLocaleString()}</div></div>
|
||
<div class="detail-item"><div class="label">Erreurs 24h</div><div class="value" style="color:${p.errors_24h > 10 ? 'var(--red)' : 'var(--green)'};">${p.errors_24h}</div></div>
|
||
<div class="detail-item"><div class="label">Date Intégration</div><div class="value">${p.integrated_at}</div></div>
|
||
<div class="detail-item"><div class="label">Méthode</div><div class="value"><span class="method-badge ${p.method}">${p.method === 'auto' ? '🤖 Auto-Discovery' : '✋ Manuel'}</span></div></div>
|
||
${p.discovered_at ? `<div class="detail-item full"><div class="label">Découvert par</div><div class="value">${p.discovery_source} le ${p.discovered_at}</div></div>` : ''}
|
||
<div class="detail-item full" onclick="window.open('${p.signup_url}','_blank')"><div class="label">URL Inscription (cliquer pour ouvrir)</div><div class="value mono" style="color:var(--cyan);">${p.signup_url}</div></div>
|
||
</div>
|
||
<div id="testResult" style="margin-top:16px;"></div>
|
||
`;
|
||
document.getElementById('providerModal').classList.add('open');
|
||
}
|
||
|
||
function showStatDetail(type) {
|
||
let title = '', content = '';
|
||
|
||
if (type === 'providers') {
|
||
title = '📊 Détails Providers';
|
||
const free = providers.filter(p => p.type === 'free');
|
||
const paid = providers.filter(p => p.type === 'paid');
|
||
const local = providers.filter(p => p.type === 'local');
|
||
content = `
|
||
<h4 style="color:var(--green);margin-bottom:10px;">✅ Gratuits (${free.length})</h4>
|
||
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:20px;">
|
||
${free.map(p => `<span onclick="closeModal('statModal');showProviderDetail('${p.name}')" style="background:rgba(34,197,94,0.1);padding:6px 12px;border-radius:6px;cursor:pointer;font-size:12px;">${icons[p.name]||'🤖'} ${p.display}</span>`).join('')}
|
||
</div>
|
||
<h4 style="color:var(--yellow);margin-bottom:10px;">💰 Payants (${paid.length})</h4>
|
||
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:20px;">
|
||
${paid.map(p => `<span onclick="closeModal('statModal');showProviderDetail('${p.name}')" style="background:rgba(234,179,8,0.1);padding:6px 12px;border-radius:6px;cursor:pointer;font-size:12px;">${icons[p.name]||'🤖'} ${p.display}</span>`).join('')}
|
||
</div>
|
||
<h4 style="color:var(--purple);margin-bottom:10px;">🖥️ Local (${local.length})</h4>
|
||
<div style="display:flex;flex-wrap:wrap;gap:8px;">
|
||
${local.map(p => `<span onclick="closeModal('statModal');showProviderDetail('${p.name}')" style="background:rgba(168,85,247,0.1);padding:6px 12px;border-radius:6px;cursor:pointer;font-size:12px;">${icons[p.name]||'🤖'} ${p.display}</span>`).join('')}
|
||
</div>
|
||
`;
|
||
} else if (type === 'scripts') {
|
||
title = '🔍 Scripts Discovery';
|
||
content = scripts.map(s => `
|
||
<div style="background:var(--bg-dark);padding:14px;border-radius:10px;margin-bottom:12px;cursor:pointer;" onclick="viewLog('${s.log}')">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
|
||
<span style="font-size:18px;">${s.icon} <strong>${s.name}</strong></span>
|
||
<span style="background:rgba(34,197,94,0.2);color:var(--green);padding:3px 10px;border-radius:10px;font-size:10px;">● Actif</span>
|
||
</div>
|
||
<div style="font-size:12px;color:var(--text-muted);margin-bottom:6px;">${s.description}</div>
|
||
<div style="font-size:11px;color:var(--text-dim);">⏰ ${s.cron_human} | 🕐 ${s.last_run}</div>
|
||
${s.discoveries > 0 ? `<div style="color:var(--pink);font-size:12px;margin-top:6px;">✨ ${s.discoveries} découvertes</div>` : ''}
|
||
</div>
|
||
`).join('');
|
||
} else if (type === 'newProviders') {
|
||
title = '✨ Nouvelles IA Découvertes (30 jours)';
|
||
const newP = providers.filter(p => new Date(p.integrated_at) > new Date(Date.now() - 30*24*60*60*1000));
|
||
content = newP.map(p => `
|
||
<div onclick="closeModal('statModal');showProviderDetail('${p.name}')" style="background:var(--bg-dark);padding:14px;border-radius:10px;margin-bottom:12px;cursor:pointer;border-left:3px solid var(--pink);">
|
||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px;">
|
||
<span style="font-size:24px;">${icons[p.name]||'🤖'}</span>
|
||
<div>
|
||
<div style="font-weight:600;">${p.display}</div>
|
||
<div style="font-size:11px;color:var(--text-muted);">${p.model}</div>
|
||
</div>
|
||
<span class="badge new" style="margin-left:auto;">NEW</span>
|
||
</div>
|
||
<div style="font-size:11px;color:var(--text-dim);">
|
||
📅 Intégré: ${p.integrated_at} |
|
||
🤖 ${p.method === 'auto' ? 'Auto-Discovery' : 'Manuel'} |
|
||
${p.discovered_at ? `🔍 Découvert: ${p.discovered_at}` : ''}
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
} else if (type === 'performance') {
|
||
title = '📈 Analytics Performance';
|
||
const total = providers.reduce((a, p) => a + p.requests_24h, 0);
|
||
const errors = providers.reduce((a, p) => a + p.errors_24h, 0);
|
||
content = `
|
||
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:20px;">
|
||
<div style="background:var(--bg-dark);padding:14px;border-radius:10px;text-align:center;">
|
||
<div style="font-size:24px;font-weight:700;color:var(--cyan);">${total.toLocaleString()}</div>
|
||
<div style="font-size:11px;color:var(--text-muted);">Requêtes 24h</div>
|
||
</div>
|
||
<div style="background:var(--bg-dark);padding:14px;border-radius:10px;text-align:center;">
|
||
<div style="font-size:24px;font-weight:700;color:var(--red);">${errors}</div>
|
||
<div style="font-size:11px;color:var(--text-muted);">Erreurs 24h</div>
|
||
</div>
|
||
<div style="background:var(--bg-dark);padding:14px;border-radius:10px;text-align:center;">
|
||
<div style="font-size:24px;font-weight:700;color:var(--green);">${((total-errors)/total*100).toFixed(1)}%</div>
|
||
<div style="font-size:11px;color:var(--text-muted);">Taux Succès</div>
|
||
</div>
|
||
</div>
|
||
<h4 style="margin-bottom:12px;">Top Providers par requêtes</h4>
|
||
${providers.sort((a,b) => b.requests_24h - a.requests_24h).slice(0,5).map((p,i) => `
|
||
<div onclick="closeModal('statModal');showProviderDetail('${p.name}')" style="display:flex;align-items:center;gap:12px;padding:10px;background:var(--bg-dark);border-radius:8px;margin-bottom:8px;cursor:pointer;">
|
||
<span style="font-size:18px;">${i+1}</span>
|
||
<span>${icons[p.name]||'🤖'}</span>
|
||
<span style="flex:1;">${p.display}</span>
|
||
<span style="color:var(--cyan);">${p.requests_24h.toLocaleString()}</span>
|
||
</div>
|
||
`).join('')}
|
||
`;
|
||
} else if (type === 'lastDiscovery') {
|
||
title = '📋 Log Dernière Discovery';
|
||
content = '<div class="log-viewer" id="statLogViewer">Chargement...</div>';
|
||
setTimeout(() => viewLogInModal('/opt/wevads/logs/ai_discovery.log'), 100);
|
||
}
|
||
|
||
document.getElementById('statModalTitle').textContent = title;
|
||
document.getElementById('statModalBody').innerHTML = content;
|
||
document.getElementById('statModal').classList.add('open');
|
||
}
|
||
|
||
async function viewLogInModal(logFile) {
|
||
try {
|
||
const r = await fetch(location.href, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({action: 'get_log', log: logFile})
|
||
});
|
||
const d = await r.json();
|
||
document.getElementById('statLogViewer').textContent = d.content || d.error || 'Vide';
|
||
} catch (e) {
|
||
document.getElementById('statLogViewer').textContent = 'Erreur: ' + e.message;
|
||
}
|
||
}
|
||
|
||
function showScriptDetail(idx) {
|
||
const s = scripts[idx];
|
||
document.getElementById('statModalTitle').textContent = s.icon + ' ' + s.name;
|
||
document.getElementById('statModalBody').innerHTML = `
|
||
<div class="detail-grid">
|
||
<div class="detail-item full"><div class="label">Description</div><div class="value">${s.description}</div></div>
|
||
<div class="detail-item" onclick="copyText('${s.script}')"><div class="label">Script</div><div class="value mono">${s.script}</div></div>
|
||
<div class="detail-item"><div class="label">CRON</div><div class="value mono">${s.cron}</div></div>
|
||
<div class="detail-item"><div class="label">Fréquence</div><div class="value">${s.cron_human}</div></div>
|
||
<div class="detail-item"><div class="label">Dernière Exécution</div><div class="value">${s.last_run}</div></div>
|
||
<div class="detail-item full" onclick="copyText('${s.log}')"><div class="label">Fichier Log</div><div class="value mono">${s.log}</div></div>
|
||
</div>
|
||
<h4 style="margin:16px 0 8px;">📋 Dernières lignes du log</h4>
|
||
<div class="log-viewer" id="scriptLogViewer">Chargement...</div>
|
||
`;
|
||
document.getElementById('statModal').classList.add('open');
|
||
setTimeout(() => viewLogInModal(s.log), 100);
|
||
}
|
||
|
||
function closeModal(id) {
|
||
document.getElementById(id).classList.remove('open');
|
||
}
|
||
|
||
async function testCurrentProvider() {
|
||
if (!currentProvider) return;
|
||
document.getElementById('testResult').innerHTML = '<div style="color:var(--cyan);">⏳ Test en cours...</div>';
|
||
try {
|
||
const r = await fetch(location.href, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({action: 'test_provider', provider: currentProvider})
|
||
});
|
||
const d = await r.json();
|
||
document.getElementById('testResult').innerHTML = d.success
|
||
? `<div style="background:rgba(34,197,94,0.1);border:1px solid var(--green);padding:12px;border-radius:8px;color:var(--green);">✅ OK - Latence: ${d.latency}</div>`
|
||
: `<div style="background:rgba(239,68,68,0.1);border:1px solid var(--red);padding:12px;border-radius:8px;color:var(--red);">❌ Erreur</div>`;
|
||
} catch (e) {
|
||
document.getElementById('testResult').innerHTML = `<div style="color:var(--red);">❌ ${e.message}</div>`;
|
||
}
|
||
}
|
||
|
||
async function runDiscovery() {
|
||
if (!confirm('Lancer le script de découverte maintenant?')) return;
|
||
try {
|
||
const r = await fetch(location.href, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({action: 'run_discovery'})
|
||
});
|
||
const d = await r.json();
|
||
alert(d.output || 'Discovery lancé!');
|
||
location.reload();
|
||
} catch (e) {
|
||
alert('Erreur: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function testAllProviders() {
|
||
for (const p of providers) {
|
||
showProviderDetail(p.name);
|
||
await testCurrentProvider();
|
||
await new Promise(r => setTimeout(r, 1000));
|
||
}
|
||
}
|
||
|
||
function copyText(text) {
|
||
navigator.clipboard.writeText(text);
|
||
alert('Copié: ' + text.substring(0, 50) + '...');
|
||
}
|
||
|
||
function copyKey(key) {
|
||
navigator.clipboard.writeText(key);
|
||
alert('Clé API copiée!');
|
||
}
|
||
|
||
// Rotation animation
|
||
let rotationIdx = 0;
|
||
setInterval(() => {
|
||
document.querySelectorAll('.rotation-item').forEach((el, i) => {
|
||
el.style.borderColor = i === rotationIdx ? 'var(--green)' : 'var(--border)';
|
||
el.style.boxShadow = i === rotationIdx ? '0 0 20px rgba(34,197,94,0.3)' : 'none';
|
||
});
|
||
rotationIdx = (rotationIdx + 1) % providers.length;
|
||
}, 3000);
|
||
|
||
// Click outside modal to close
|
||
document.querySelectorAll('.modal-overlay').forEach(m => {
|
||
m.addEventListener('click', e => { if (e.target === m) m.classList.remove('open'); });
|
||
});
|
||
</script>
|
||
|
||
|
||
|
||
</body>
|
||
</html>
|