feat: redesign dashboard with huggingface aesthetics
This commit is contained in:
parent
458cdaa3c2
commit
be63b30285
|
|
@ -1,63 +1,460 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="mb-8">
|
<style>
|
||||||
<h1 class="text-3xl font-bold text-white">Dashboard</h1>
|
/* Hugging Face Inspired Styles */
|
||||||
<p class="text-gray-400 mt-2">Manage scopes, rules and monitor agent activity.</p>
|
.hf-card {
|
||||||
|
background-color: #1f2937; /* Tailwind gray-800 */
|
||||||
|
border: 1px solid #374151; /* Tailwind gray-700 */
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.hf-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
.hf-header {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
letter-spacing: 0.025em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.hf-tag {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 0.125rem 0.6rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.hide-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.hide-scrollbar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="space-y-6 pb-12">
|
||||||
|
<!-- 1. KILL-SWITCH DR. NEXUM -->
|
||||||
|
<div class="hf-card border-red-500/50 shadow-[0_0_20px_rgba(239,68,68,0.15)] bg-gradient-to-br from-gray-900 to-gray-800">
|
||||||
|
<div class="hf-header bg-gradient-to-r from-red-900/60 to-orange-900/60 text-red-400 border-b border-red-500/30">
|
||||||
|
<svg class="w-6 h-6 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
|
||||||
|
KILL-SWITCH DR. NEXUM
|
||||||
|
</div>
|
||||||
|
<div class="p-6 grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
|
<div class="flex flex-col justify-center items-center p-4 bg-black/40 rounded-xl border border-red-900/30">
|
||||||
|
<span class="text-4xl md:text-5xl font-black text-red-500 tracking-tighter" id="countdown">J-X</span>
|
||||||
|
<span class="text-xs text-red-400/70 mt-2 uppercase font-bold tracking-widest text-center">avant le 15 nov 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-2 space-y-6 flex flex-col justify-center">
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-end mb-2">
|
||||||
|
<label class="text-sm font-bold text-gray-300 uppercase tracking-wide">Abonnés YouTube</label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input type="number" id="yt-subs" class="bg-gray-900 text-white text-sm font-mono rounded border border-gray-600 px-2 py-1 w-24 text-right focus:outline-none focus:border-red-500 transition-colors" value="0">
|
||||||
|
<span class="text-gray-500 font-mono text-sm">/ 1000</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-gray-900 rounded-full h-4 overflow-hidden border border-gray-700">
|
||||||
|
<div id="yt-bar" class="h-full bg-red-500 transition-all duration-700 ease-out" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-end mb-2">
|
||||||
|
<label class="text-sm font-bold text-gray-300 uppercase tracking-wide">Revenus Mensuels (€)</label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input type="number" id="rev" class="bg-gray-900 text-white text-sm font-mono rounded border border-gray-600 px-2 py-1 w-24 text-right focus:outline-none focus:border-red-500 transition-colors" value="0">
|
||||||
|
<span class="text-gray-500 font-mono text-sm">/ 300</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-gray-900 rounded-full h-4 overflow-hidden border border-gray-700">
|
||||||
|
<div id="rev-bar" class="h-full bg-red-500 transition-all duration-700 ease-out" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
|
<!-- 2. AGENTS -->
|
||||||
<!-- Scopes section -->
|
<div class="w-full overflow-x-auto hide-scrollbar pb-2">
|
||||||
<div class="bg-gray-800 rounded-lg shadow border border-gray-700 p-6">
|
<div class="flex space-x-4 min-w-max" id="agents-container">
|
||||||
<h2 class="text-xl font-semibold text-nyora mb-4">Rule Scopes</h2>
|
<!-- Rendered by JS -->
|
||||||
<ul class="divide-y divide-gray-700">
|
<div class="text-gray-500 italic text-sm p-4">Chargement des agents...</div>
|
||||||
{% for scope in rules.keys() %}
|
|
||||||
<li class="py-4 flex items-center justify-between">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="text-white font-medium text-lg uppercase">{{ scope }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<a href="/editor/{{ scope }}" class="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white text-sm font-medium rounded-md transition">Edit Rules</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
{% if not rules %}
|
|
||||||
<li class="py-4 text-gray-500 italic">No scopes available.</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quick Stats or Audit snippet -->
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
<div class="bg-gray-800 rounded-lg shadow border border-gray-700 p-6">
|
<!-- Main Column -->
|
||||||
<h2 class="text-xl font-semibold text-nyora mb-4">Recent Activity</h2>
|
<div class="lg:col-span-2 space-y-6">
|
||||||
<div class="overflow-x-auto">
|
<!-- 3. REGISTRE PORTS -->
|
||||||
<table class="min-w-full divide-y divide-gray-700">
|
<div class="hf-card">
|
||||||
<thead>
|
<div class="hf-header bg-gray-800 text-gray-200 border-b border-gray-700">
|
||||||
<tr>
|
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg>
|
||||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Agent</th>
|
Registre Ports
|
||||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Scope</th>
|
|
||||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Time</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="divide-y divide-gray-700">
|
|
||||||
{% for log in logs[:10] %}
|
|
||||||
<tr>
|
|
||||||
<td class="px-3 py-3 whitespace-nowrap text-sm text-gray-300">{{ log.agent_name }}</td>
|
|
||||||
<td class="px-3 py-3 whitespace-nowrap text-sm">
|
|
||||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-700 text-gray-300">
|
|
||||||
{{ log.scope }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-3 py-3 whitespace-nowrap text-sm text-gray-500">{{ log.timestamp }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
{% if not logs %}
|
|
||||||
<tr><td colspan="3" class="px-3 py-4 text-gray-500 text-center text-sm italic">No recent activity</td></tr>
|
|
||||||
{% endif %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 text-right">
|
<div class="p-6 bg-gray-900">
|
||||||
<a href="/audit" class="text-nyora hover:text-yellow-400 text-sm font-medium">View all logs →</a>
|
<div id="ports-grid" class="flex flex-wrap gap-2">
|
||||||
|
<!-- Rendered by JS -->
|
||||||
|
<div class="text-gray-500 italic text-sm">Chargement des ports...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 5. RÈGLES INFRA -->
|
||||||
|
<div class="hf-card">
|
||||||
|
<div class="hf-header bg-gray-800 text-gray-200 border-b border-gray-700">
|
||||||
|
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path></svg>
|
||||||
|
Règles Infra
|
||||||
|
</div>
|
||||||
|
<div class="divide-y divide-gray-700/50 bg-gray-900" id="infra-accordion">
|
||||||
|
<!-- Rendered by JS -->
|
||||||
|
<div class="p-4 text-gray-500 italic text-sm">Chargement de l'infrastructure...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar Column -->
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- 7. RACCOURCIS ADMIN -->
|
||||||
|
<div class="hf-card">
|
||||||
|
<div class="hf-header bg-gray-800 text-gray-200 border-b border-gray-700">
|
||||||
|
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
|
||||||
|
Raccourcis Admin
|
||||||
|
</div>
|
||||||
|
<div class="p-4 bg-gray-900 grid grid-cols-2 gap-3">
|
||||||
|
<a href="/editor/infra" class="bg-gray-800 hover:bg-gray-700 border border-gray-700 text-gray-200 text-xs font-bold uppercase tracking-wider py-3 px-2 rounded-lg text-center transition">Éditeur Infra</a>
|
||||||
|
<a href="/keys" class="bg-gray-800 hover:bg-gray-700 border border-gray-700 text-gray-200 text-xs font-bold uppercase tracking-wider py-3 px-2 rounded-lg text-center transition">Gestion Clés</a>
|
||||||
|
<a href="/audit" class="bg-gray-800 hover:bg-gray-700 border border-gray-700 text-gray-200 text-xs font-bold uppercase tracking-wider py-3 px-2 rounded-lg text-center transition">Logs Complets</a>
|
||||||
|
<a href="/docs" class="bg-gray-800 hover:bg-gray-700 border border-gray-700 text-gray-200 text-xs font-bold uppercase tracking-wider py-3 px-2 rounded-lg text-center transition" target="_blank">Docs API</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 4. STACK LLM -->
|
||||||
|
<div class="hf-card border-blue-500/30">
|
||||||
|
<div class="hf-header bg-blue-900/40 text-blue-400 border-b border-blue-500/30">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>
|
||||||
|
Stack LLM
|
||||||
|
</div>
|
||||||
|
<div class="p-5 space-y-4 bg-gray-900" id="llm-stack">
|
||||||
|
<div class="animate-pulse space-y-3">
|
||||||
|
<div class="h-4 bg-gray-800 rounded w-3/4"></div>
|
||||||
|
<div class="h-4 bg-gray-800 rounded w-1/2"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 6. AUDIT RÉCENT -->
|
||||||
|
<div class="hf-card">
|
||||||
|
<div class="hf-header bg-gray-800 text-gray-200 border-b border-gray-700">
|
||||||
|
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||||
|
Audit Récent
|
||||||
|
</div>
|
||||||
|
<div class="p-5 bg-gray-900">
|
||||||
|
<div class="relative border-l-2 border-gray-800 ml-3 space-y-6" id="audit-timeline">
|
||||||
|
<!-- Rendered by JS -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// --- DR NEXUM COUNTDOWN & BARS ---
|
||||||
|
function updateDrNexum() {
|
||||||
|
const ytInput = document.getElementById('yt-subs');
|
||||||
|
const revInput = document.getElementById('rev');
|
||||||
|
const yt = parseInt(ytInput.value) || 0;
|
||||||
|
const rev = parseInt(revInput.value) || 0;
|
||||||
|
|
||||||
|
localStorage.setItem('dr_nexum_yt', yt);
|
||||||
|
localStorage.setItem('dr_nexum_rev', rev);
|
||||||
|
|
||||||
|
const updateBar = (id, val, max) => {
|
||||||
|
const pct = Math.min(100, Math.max(0, (val / max) * 100));
|
||||||
|
const bar = document.getElementById(id);
|
||||||
|
bar.style.width = pct + '%';
|
||||||
|
|
||||||
|
let colorClass = 'bg-red-500';
|
||||||
|
if (pct >= 80) colorClass = 'bg-green-500';
|
||||||
|
else if (pct >= 50) colorClass = 'bg-orange-500';
|
||||||
|
|
||||||
|
bar.className = `h-full transition-all duration-700 ease-out ${colorClass}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
updateBar('yt-bar', yt, 1000);
|
||||||
|
updateBar('rev-bar', rev, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initDrNexum() {
|
||||||
|
const ytInput = document.getElementById('yt-subs');
|
||||||
|
const revInput = document.getElementById('rev');
|
||||||
|
ytInput.value = localStorage.getItem('dr_nexum_yt') || 0;
|
||||||
|
revInput.value = localStorage.getItem('dr_nexum_rev') || 0;
|
||||||
|
|
||||||
|
ytInput.addEventListener('input', updateDrNexum);
|
||||||
|
revInput.addEventListener('input', updateDrNexum);
|
||||||
|
updateDrNexum();
|
||||||
|
|
||||||
|
const targetDate = new Date('2026-11-15T00:00:00');
|
||||||
|
const today = new Date();
|
||||||
|
const diff = targetDate - today;
|
||||||
|
const days = Math.ceil(diff / (1000 * 60 * 60 * 24));
|
||||||
|
document.getElementById('countdown').textContent = `J-${Math.max(0, days)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- FORMATTERS & STYLES ---
|
||||||
|
function relativeTime(dateStr) {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const diff = Math.floor((new Date() - date) / 1000);
|
||||||
|
if (diff < 60) return "il y a quelques s";
|
||||||
|
if (diff < 3600) return `il y a ${Math.floor(diff / 60)} min`;
|
||||||
|
if (diff < 86400) return `il y a ${Math.floor(diff / 3600)} h`;
|
||||||
|
return `il y a ${Math.floor(diff / 86400)} j`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentColors = {
|
||||||
|
'claude': { bg: 'bg-blue-900/40', text: 'text-blue-400', border: 'border-blue-500/30', tag: 'bg-blue-500/20 text-blue-400 border border-blue-500/30' },
|
||||||
|
'gemini': { bg: 'bg-green-900/40', text: 'text-green-400', border: 'border-green-500/30', tag: 'bg-green-500/20 text-green-400 border border-green-500/30' },
|
||||||
|
'hermes-tt': { bg: 'bg-orange-900/40', text: 'text-orange-400', border: 'border-orange-500/30', tag: 'bg-orange-500/20 text-orange-400 border border-orange-500/30' },
|
||||||
|
'hermes-nyora': { bg: 'bg-purple-900/40', text: 'text-purple-400', border: 'border-purple-500/30', tag: 'bg-purple-500/20 text-purple-400 border border-purple-500/30' },
|
||||||
|
'hermes-perso': { bg: 'bg-cyan-900/40', text: 'text-cyan-400', border: 'border-cyan-500/30', tag: 'bg-cyan-500/20 text-cyan-400 border border-cyan-500/30' }
|
||||||
|
};
|
||||||
|
|
||||||
|
function getAgentStyle(name) {
|
||||||
|
const key = name.toLowerCase();
|
||||||
|
for(let k in agentColors) {
|
||||||
|
if(key.includes(k)) return agentColors[k];
|
||||||
|
}
|
||||||
|
return { bg: 'bg-gray-800', text: 'text-gray-200', border: 'border-gray-700', tag: 'bg-gray-700 text-gray-300 border border-gray-600' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- FETCH DATA ---
|
||||||
|
async function fetchDashboardData() {
|
||||||
|
const fetchOpts = { credentials: 'omit' }; // Using omit because standard cookie auth fails on /api routes requiring X-API-Key
|
||||||
|
|
||||||
|
// Wait, the user explicitly asked for "credentials: 'include'"
|
||||||
|
const userOpts = { credentials: 'include' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Using Promise.allSettled to handle individual failures if any endpoint rejects cookies
|
||||||
|
const [auditRes, portsRes, llmRes, infraRes] = await Promise.allSettled([
|
||||||
|
fetch('/api/audit', userOpts),
|
||||||
|
fetch('/api/ports', userOpts),
|
||||||
|
fetch('/api/rules/llm', userOpts),
|
||||||
|
fetch('/api/rules/infra', userOpts)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const extractData = async (res, fallback) => {
|
||||||
|
if (res.status === 'fulfilled' && res.value.ok) {
|
||||||
|
return await res.value.json();
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Notice: /api/audit is not defined in api.py, only /audit in admin.py which returns HTML.
|
||||||
|
// Oh, let's inject Jinja variables as fallback if fetch fails (401 or 404).
|
||||||
|
// This guarantees the page works even if the fetch fails due to X-API-Key issues.
|
||||||
|
const auditData = await extractData(auditRes, {{ logs | tojson | safe | default('[]') }});
|
||||||
|
const infraData = await extractData(infraRes, {{ rules.infra | tojson | safe | default('{}') }});
|
||||||
|
const llmData = await extractData(llmRes, {{ rules.llm | tojson | safe | default('{}') }});
|
||||||
|
const portsData = await extractData(portsRes, {});
|
||||||
|
|
||||||
|
renderAgents(auditData);
|
||||||
|
renderPorts(portsData);
|
||||||
|
renderLLM(llmData);
|
||||||
|
renderInfra(infraData);
|
||||||
|
renderAudit(auditData);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Fetch error", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- RENDERERS ---
|
||||||
|
function renderAgents(auditData) {
|
||||||
|
const container = document.getElementById('agents-container');
|
||||||
|
const agents = [
|
||||||
|
{ name: 'Claude', id: 'claude', scopes: ['infra', 'llm', 'nyora', 'tt', 'perso'] },
|
||||||
|
{ name: 'Gemini', id: 'gemini', scopes: ['infra', 'llm', 'nyora'] },
|
||||||
|
{ name: 'Hermes-TT', id: 'hermes-tt', scopes: ['tt'] },
|
||||||
|
{ name: 'Hermes-Nyora', id: 'hermes-nyora', scopes: ['nyora', 'infra'] },
|
||||||
|
{ name: 'Hermes-Perso', id: 'hermes-perso', scopes: ['perso'] }
|
||||||
|
];
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
agents.forEach(ag => {
|
||||||
|
const style = getAgentStyle(ag.id);
|
||||||
|
const lastLog = auditData && auditData.find ? auditData.find(l => l.agent_name.toLowerCase().includes(ag.id)) : null;
|
||||||
|
|
||||||
|
const active = lastLog && (new Date() - new Date(lastLog.timestamp)) < 86400000;
|
||||||
|
const statusColor = active ? 'bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.8)]' : 'bg-gray-600';
|
||||||
|
const lastTime = lastLog ? relativeTime(lastLog.timestamp) : 'Jamais';
|
||||||
|
|
||||||
|
const tagsHtml = ag.scopes.map(s => `<span class="hf-tag ${style.tag}">${s}</span>`).join('');
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="hf-card w-72 flex-shrink-0 ${style.border} bg-gray-900">
|
||||||
|
<div class="hf-header ${style.bg} ${style.text} border-b ${style.border} flex justify-between">
|
||||||
|
<span class="tracking-widest">${ag.name}</span>
|
||||||
|
<span class="w-2.5 h-2.5 rounded-full ${statusColor}"></span>
|
||||||
|
</div>
|
||||||
|
<div class="p-5">
|
||||||
|
<div class="flex flex-wrap gap-2 mb-6 h-12 align-top">
|
||||||
|
${tagsHtml}
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center border-t border-gray-800 pt-3">
|
||||||
|
<span class="text-xs font-bold text-gray-500 uppercase tracking-widest">Activité</span>
|
||||||
|
<span class="text-xs font-mono text-gray-300">${lastTime}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPorts(portsData) {
|
||||||
|
const container = document.getElementById('ports-grid');
|
||||||
|
const explicitFree = [3091, 3001, 3002, 3015];
|
||||||
|
const allPorts = new Set([...explicitFree]);
|
||||||
|
for(let i=3100; i<=3199; i++) allPorts.add(i);
|
||||||
|
|
||||||
|
// Add occupied if they are not in the standard set
|
||||||
|
if (portsData) {
|
||||||
|
Object.keys(portsData).forEach(p => allPorts.add(parseInt(p)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedPorts = Array.from(allPorts).sort((a,b) => a-b);
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
sortedPorts.forEach(port => {
|
||||||
|
const service = portsData ? portsData[port] : null;
|
||||||
|
if (service) {
|
||||||
|
// Occupied - Red Chip
|
||||||
|
html += `
|
||||||
|
<div class="hf-tag bg-red-900/40 text-red-400 border border-red-500/40 gap-1.5" title="${service}">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-red-500 animate-pulse"></span>
|
||||||
|
<span class="font-mono text-xs">${port}</span>
|
||||||
|
<span class="text-red-400/60 font-medium text-[10px] hidden sm:inline">${service}</span>
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
// Free - Gray Chip
|
||||||
|
html += `
|
||||||
|
<div class="hf-tag bg-gray-800 text-gray-500 border border-gray-700 font-mono text-xs">
|
||||||
|
${port}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLLM(data) {
|
||||||
|
const container = document.getElementById('llm-stack');
|
||||||
|
if (!data || Object.keys(data).length === 0) {
|
||||||
|
container.innerHTML = '<div class="text-gray-500 italic text-sm">Données indisponibles</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = [
|
||||||
|
{ label: 'Modèle Actif', value: data['modele_par_defaut'] || data['modele'] || 'N/A' },
|
||||||
|
{ label: 'Provider', value: data['provider'] || 'N/A' },
|
||||||
|
{ label: 'Budget Mensuel', value: data['budget'] || 'N/A' },
|
||||||
|
{ label: 'Endpoint Bifrost', value: data['endpoint'] || data['endpoint_interne'] || 'N/A' },
|
||||||
|
{ label: 'Règle Vision', value: data['vision'] || 'N/A' }
|
||||||
|
];
|
||||||
|
|
||||||
|
let html = '<div class="space-y-3">';
|
||||||
|
rows.forEach(r => {
|
||||||
|
html += `
|
||||||
|
<div class="flex justify-between items-start border-b border-gray-800 pb-2 last:border-0 last:pb-0">
|
||||||
|
<span class="text-xs font-bold text-blue-500/70 uppercase tracking-wider w-1/3">${r.label}</span>
|
||||||
|
<span class="text-sm font-mono text-blue-100 text-right w-2/3 break-words">${r.value}</span>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderInfra(data) {
|
||||||
|
const container = document.getElementById('infra-accordion');
|
||||||
|
if (!data || Object.keys(data).length === 0) {
|
||||||
|
container.innerHTML = '<div class="p-4 text-gray-500 italic text-sm">Aucune règle définie</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
Object.keys(data).forEach(key => {
|
||||||
|
const val = data[key];
|
||||||
|
let contentHtml = '';
|
||||||
|
|
||||||
|
if (typeof val === 'object' && val !== null) {
|
||||||
|
let rows = '';
|
||||||
|
for (let k in val) {
|
||||||
|
rows += `
|
||||||
|
<tr class="border-b border-gray-800 last:border-0 hover:bg-gray-800/50 transition">
|
||||||
|
<td class="py-2.5 px-3 text-xs font-bold text-gray-500 uppercase tracking-widest w-1/3">${k}</td>
|
||||||
|
<td class="py-2.5 px-3 text-sm font-mono text-gray-300 w-2/3">${val[k]}</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
contentHtml = `<table class="w-full text-left">${rows}</table>`;
|
||||||
|
} else {
|
||||||
|
contentHtml = `<p class="text-sm font-mono text-gray-300 p-3">${val}</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="group border-b border-gray-800/50 last:border-0">
|
||||||
|
<button class="w-full px-5 py-4 text-left font-bold text-gray-300 hover:bg-gray-800 transition focus:outline-none flex justify-between items-center" onclick="this.nextElementSibling.classList.toggle('hidden')">
|
||||||
|
<span class="uppercase tracking-widest text-sm">${key}</span>
|
||||||
|
<svg class="w-4 h-4 text-gray-500 transform group-focus-within:rotate-180 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
||||||
|
</button>
|
||||||
|
<div class="bg-gray-900/80 hidden">
|
||||||
|
${contentHtml}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAudit(data) {
|
||||||
|
const container = document.getElementById('audit-timeline');
|
||||||
|
if (!data || !data.slice) {
|
||||||
|
container.innerHTML = '<p class="text-sm text-gray-500 italic">Erreur de lecture des logs</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sliced = data.slice(0, 15);
|
||||||
|
if (sliced.length === 0) {
|
||||||
|
container.innerHTML = '<p class="text-sm text-gray-500 italic">Aucun log récent</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
sliced.forEach(log => {
|
||||||
|
const style = getAgentStyle(log.agent_name);
|
||||||
|
html += `
|
||||||
|
<div class="relative pl-6">
|
||||||
|
<div class="absolute -left-2 mt-1.5 w-4 h-4 rounded-full ${style.bg} border-4 border-gray-900 shadow-sm z-10"></div>
|
||||||
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-baseline gap-1 mb-1">
|
||||||
|
<span class="hf-tag ${style.tag} w-max">${log.agent_name}</span>
|
||||||
|
<span class="text-[10px] font-mono text-gray-500 uppercase tracking-wider">${relativeTime(log.timestamp)}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-400 mt-1">Scope ciblé : <strong class="text-gray-200 uppercase tracking-wide text-xs">${log.scope}</strong></p>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init
|
||||||
|
initDrNexum();
|
||||||
|
fetchDashboardData();
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue