Add monitoring dashboard with live system visualization

Introduce an interactive SVG-based monitoring dashboard that displays:
- Central BotServer node with animated status indicators
- Service nodes for PostgreSQL, Qdrant, MinIO, BotModels, Cache, and
  Vault
- Animated data flow connections between services
- Real-time metrics panels (sessions, messages, response time)
- Resource utilization bars (CPU, memory, GPU, disk)
- Live activity ticker

The HTML version includes
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-12-01 08:35:47 -03:00
parent 09ccb5e0dd
commit 55d092472b
2 changed files with 915 additions and 0 deletions

View file

@ -0,0 +1,539 @@
<div class="monitoring-home" id="monitoring-home">
<!-- Live System Visualization -->
<div class="live-visualization">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 700" class="live-svg">
<defs>
<!-- Gradients -->
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:var(--bg-dark, #0f172a)"/>
<stop offset="50%" style="stop-color:var(--bg-surface, #1e293b)"/>
<stop offset="100%" style="stop-color:var(--bg-dark, #0f172a)"/>
</linearGradient>
<linearGradient id="coreGlow" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3b82f6"/>
<stop offset="50%" style="stop-color:#6366f1"/>
<stop offset="100%" style="stop-color:#8b5cf6"/>
</linearGradient>
<linearGradient id="greenGlow" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#10b981"/>
<stop offset="100%" style="stop-color:#22c55e"/>
</linearGradient>
<linearGradient id="amberGlow" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#f59e0b"/>
<stop offset="100%" style="stop-color:#fbbf24"/>
</linearGradient>
<linearGradient id="redGlow" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#ef4444"/>
<stop offset="100%" style="stop-color:#f87171"/>
</linearGradient>
<linearGradient id="dbGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3b82f6"/>
<stop offset="100%" style="stop-color:#1d4ed8"/>
</linearGradient>
<linearGradient id="vectorGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#8b5cf6"/>
<stop offset="100%" style="stop-color:#6d28d9"/>
</linearGradient>
<linearGradient id="storageGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#f59e0b"/>
<stop offset="100%" style="stop-color:#d97706"/>
</linearGradient>
<linearGradient id="aiGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#ec4899"/>
<stop offset="100%" style="stop-color:#db2777"/>
</linearGradient>
<linearGradient id="cacheGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#06b6d4"/>
<stop offset="100%" style="stop-color:#0891b2"/>
</linearGradient>
<!-- Filters -->
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="softGlow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="6" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Background -->
<rect width="100%" height="100%" fill="url(#bgGradient)"/>
<!-- Subtle grid -->
<g opacity="0.08" stroke="#64748b" stroke-width="0.5">
<line x1="0" y1="175" x2="1200" y2="175"/>
<line x1="0" y1="350" x2="1200" y2="350"/>
<line x1="0" y1="525" x2="1200" y2="525"/>
<line x1="300" y1="0" x2="300" y2="700"/>
<line x1="600" y1="0" x2="600" y2="700"/>
<line x1="900" y1="0" x2="900" y2="700"/>
</g>
<!-- ==================== CENTRAL CORE: BotServer ==================== -->
<g transform="translate(600, 350)" class="core-node">
<!-- Outer rotating ring -->
<circle r="90" fill="none" stroke="url(#coreGlow)" stroke-width="2" stroke-dasharray="10 5" opacity="0.4" class="rotate-slow"/>
<!-- Middle pulsing ring -->
<circle r="70" fill="none" stroke="url(#coreGlow)" stroke-width="3" opacity="0.6" class="pulse-ring"/>
<!-- Core background -->
<circle r="55" fill="#1e293b" stroke="url(#coreGlow)" stroke-width="3" filter="url(#glow)"/>
<!-- Bot icon -->
<g fill="#f8fafc">
<rect x="-25" y="-20" width="50" height="35" rx="8" fill="url(#coreGlow)" opacity="0.9"/>
<circle cx="-10" cy="-5" r="5" fill="#f8fafc"/>
<circle cx="10" cy="-5" r="5" fill="#f8fafc"/>
<path d="M-12 10 Q0 18 12 10" stroke="#f8fafc" stroke-width="2.5" fill="none"/>
<rect x="-3" y="-35" width="6" height="12" rx="3" fill="url(#coreGlow)"/>
<circle cx="0" cy="-38" r="5" fill="url(#coreGlow)" class="antenna-pulse"/>
</g>
<text y="80" text-anchor="middle" fill="#f8fafc" font-family="system-ui" font-size="14" font-weight="600">BotServer</text>
<text y="98" text-anchor="middle" fill="#10b981" font-family="system-ui" font-size="11" class="status-text" data-service="core">● Running</text>
</g>
<!-- ==================== LEFT: Data Layer ==================== -->
<!-- PostgreSQL -->
<g transform="translate(150, 200)" class="service-node" data-service="postgresql">
<circle r="45" fill="#1e293b" stroke="url(#dbGradient)" stroke-width="2" filter="url(#glow)"/>
<g transform="translate(-15, -18)">
<ellipse cx="15" cy="0" rx="18" ry="6" fill="url(#dbGradient)"/>
<path d="M-3 0 L-3 20 Q15 30 33 20 L33 0" fill="url(#dbGradient)" opacity="0.8"/>
<ellipse cx="15" cy="20" rx="18" ry="6" fill="url(#dbGradient)"/>
<line x1="-3" y1="7" x2="33" y2="7" stroke="#1e40af" stroke-width="1"/>
<line x1="-3" y1="14" x2="33" y2="14" stroke="#1e40af" stroke-width="1"/>
</g>
<text y="65" text-anchor="middle" fill="#f8fafc" font-family="system-ui" font-size="12" font-weight="500">PostgreSQL</text>
<circle cx="35" cy="-35" r="6" class="status-dot running" data-status="postgresql"/>
</g>
<!-- Qdrant -->
<g transform="translate(150, 350)" class="service-node" data-service="qdrant">
<circle r="45" fill="#1e293b" stroke="url(#vectorGradient)" stroke-width="2" filter="url(#glow)"/>
<g fill="url(#vectorGradient)">
<polygon points="0,-22 19,11 -19,11" opacity="0.9"/>
<polygon points="0,-12 12,7 -12,7" fill="#1e293b"/>
<circle cx="0" cy="-22" r="4"/>
<circle cx="19" cy="11" r="4"/>
<circle cx="-19" cy="11" r="4"/>
<line x1="0" y1="-18" x2="0" y2="-8" stroke="#c4b5fd" stroke-width="1.5"/>
<line x1="15" y1="9" x2="8" y2="5" stroke="#c4b5fd" stroke-width="1.5"/>
<line x1="-15" y1="9" x2="-8" y2="5" stroke="#c4b5fd" stroke-width="1.5"/>
</g>
<text y="65" text-anchor="middle" fill="#f8fafc" font-family="system-ui" font-size""="12" font-weight="500">Qdrant</text>
<circle cx="35" cy="-35" r="6" class="status-dot running" data-status="qdrant"/>
</g>
<!-- MinIO -->
<g transform="translate(150, 500)" class="service-node" data-service="drive">
<circle r="45" fill="#1e293b" stroke="url(#storageGradient)" stroke-width="2" filter="url(#glow)"/>
<g fill="url(#storageGradient)">
<rect x="-22" y="-18" width="44" height="36" rx="4"/>
<rect x="-18" y="-14" width="36" height="28" rx="2" fill="#1e293b"/>
<rect x="-14" y="-10" width="28" height="8" rx="1" fill="#fcd34d" opacity="0.4"/>
<rect x="-14" y="2" width="28" height="8" rx="1" fill="#fcd34d" opacity="0.4"/>
<circle cx="10" cy="-6" r="2" fill="#fbbf24"/>
<circle cx="10" cy="6" r="2" fill="#fbbf24"/>
</g>
<text y="65" text-anchor="middle" fill="#f8fafc" font-family="system-ui" font-size="12" font-weight="500">MinIO</text>
<circle cx="35" cy="-35" r="6" class="status-dot running" data-status="drive"/>
</g>
<!-- ==================== RIGHT: Services ==================== -->
<!-- BotModels -->
<g transform="translate(1050, 200)" class="service-node" data-service="botmodels">
<circle r="45" fill="#1e293b" stroke="url(#aiGradient)" stroke-width="2" filter="url(#glow)"/>
<g>
<circle cx="0" cy="-8" r="14" fill="none" stroke="url(#aiGradient)" stroke-width="2"/>
<circle cx="0" cy="-8" r="6" fill="url(#aiGradient)" class="ai-pulse"/>
<path d="M-18 12 L0 -2 L18 12" stroke="url(#aiGradient)" stroke-width="2" fill="none"/>
<path d="M-12 18 L0 8 L12 18" stroke="url(#aiGradient)" stroke-width="2" fill="none"/>
</g>
<text y="65" text-anchor="middle" fill="#f8fafc" font-family="system-ui" font-size="12" font-weight="500">BotModels</text>
<circle cx="35" cy="-35" r="6" class="status-dot running" data-status="botmodels"/>
</g>
<!-- Cache -->
<g transform="translate(1050, 350)" class="service-node" data-service="cache">
<circle r="45" fill="#1e293b" stroke="url(#cacheGradient)" stroke-width="2" filter="url(#glow)"/>
<g fill="url(#cacheGradient)">
<rect x="-20" y="-18" width="40" height="36" rx="3"/>
<rect x="-16" y="-14" width="32" height="28" rx="2" fill="#1e293b"/>
<text x="0" y="5" text-anchor="middle" font-family="system-ui" font-size="18" fill="#22d3ee"></text>
</g>
<text y="65" text-anchor="middle" fill="#f8fafc" font-family="system-ui" font-size="12" font-weight="500">Cache</text>
<circle cx="35" cy="-35" r="6" class="status-dot running" data-status="cache"/>
</g>
<!-- Vault -->
<g transform="translate(1050, 500)" class="service-node" data-service="vault">
<circle r="45" fill="#1e293b" stroke="url(#greenGlow)" stroke-width="2" filter="url(#glow)"/>
<g fill="url(#greenGlow)">
<rect x="-18" y="-10" width="36" height="28" rx="4"/>
<rect x="-14" y="-6" width="28" height="20" rx="2" fill="#1e293b"/>
<circle cx="0" cy="4" r="6" fill="url(#greenGlow)"/>
<rect x="-2" y="4" width="4" height="8" fill="url(#greenGlow)"/>
<path d="M-10 -18 L0 -26 L10 -18" stroke="url(#greenGlow)" stroke-width="3" fill="none"/>
</g>
<text y="65" text-anchor="middle" fill="#f8fafc" font-family="system-ui" font-size="12" font-weight="500">Vault</text>
<circle cx="35" cy="-35" r="6" class="status-dot running" data-status="vault"/>
</g>
<!-- ==================== ANIMATED CONNECTION LINES ==================== -->
<!-- Left connections -->
<g class="connection-lines" stroke-width="2" fill="none">
<path d="M195 200 Q400 200 505 320" stroke="#3b82f6" opacity="0.3" class="connection-path"/>
<circle r="4" fill="#60a5fa" class="data-packet">
<animateMotion dur="3s" repeatCount="indefinite" path="M195 200 Q400 200 505 320"/>
</circle>
<path d="M195 350 L505 350" stroke="#8b5cf6" opacity="0.3" class="connection-path"/>
<circle r="4" fill="#a78bfa" class="data-packet">
<animateMotion dur="2.5s" repeatCount="indefinite" path="M195 350 L505 350"/>
</circle>
<path d="M195 500 Q400 500 505 380" stroke="#f59e0b" opacity="0.3" class="connection-path"/>
<circle r="4" fill="#fcd34d" class="data-packet">
<animateMotion dur="3.5s" repeatCount="indefinite" path="M195 500 Q400 500 505 380"/>
</circle>
</g>
<!-- Right connections -->
<g class="connection-lines" stroke-width="2" fill="none">
<path d="M695 320 Q800 200 1005 200" stroke="#ec4899" opacity="0.3" class="connection-path"/>
<circle r="4" fill="#f472b6" class="data-packet">
<animateMotion dur="2s" repeatCount="indefinite" path="M695 320 Q800 200 1005 200"/>
</circle>
<path d="M695 350 L1005 350" stroke="#06b6d4" opacity="0.3" class="connection-path"/>
<circle r="4" fill="#22d3ee" class="data-packet">
<animateMotion dur="1.5s" repeatCount="indefinite" path="M695 350 L1005 350"/>
</circle>
<path d="M695 380 Q800 500 1005 500" stroke="#10b981" opacity="0.3" class="connection-path"/>
<circle r="4" fill="#34d399" class="data-packet">
<animateMotion dur="4s" repeatCount="indefinite" path="M695 380 Q800 500 1005 500"/>
</circle>
</g>
<!-- ==================== METRICS PANELS ==================== -->
<!-- Sessions -->
<g transform="translate(320, 80)">
<rect x="0" y="0" width="160" height="85" rx="10" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="12" y="24" fill="#64748b" font-family="system-ui" font-size="10" font-weight="500">ACTIVE SESSIONS</text>
<text x="12" y="58" fill="#f8fafc" font-family="system-ui" font-size="32" font-weight="700"
hx-get="/api/monitoring/metric/sessions"
hx-trigger="load, every 5s"
hx-swap="innerHTML">--</text>
<text x="148" y="70" fill="#10b981" font-family="system-ui" font-size="11" text-anchor="end"
hx-get="/api/monitoring/trend/sessions"
hx-trigger="load, every 5s"
hx-swap="innerHTML">↑ 0%</text>
</g>
<!-- Messages -->
<g transform="translate(500, 80)">
<rect x="0" y="0" width="160" height="85" rx="10" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="12" y="24" fill="#64748b" font-family="system-ui" font-size="10" font-weight="500">MESSAGES TODAY</text>
<text x="12" y="58" fill="#f8fafc" font-family="system-ui" font-size="32" font-weight="700"
hx-get="/api/monitoring/metric/messages"
hx-trigger="load, every 10s"
hx-swap="innerHTML">--</text>
<text x="148" y="70" fill="#94a3b8" font-family="system-ui" font-size="11" text-anchor="end"
hx-get="/api/monitoring/rate/messages"
hx-trigger="load, every 10s"
hx-swap="innerHTML">0/hr</text>
</g>
<!-- Response Time -->
<g transform="translate(680, 80)">
<rect x="0" y="0" width="160" height="85" rx="10" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="12" y="24" fill="#64748b" font-family="system-ui" font-size="10" font-weight="500">AVG RESPONSE</text>
<text x="12" y="58" fill="#f8fafc" font-family="system-ui" font-size="32" font-weight="700"
hx-get="/api/monitoring/metric/response_time"
hx-trigger="load, every 10s"
hx-swap="innerHTML">--</text>
<text x="148" y="70" fill="#94a3b8" font-family="system-ui" font-size="11" text-anchor="end">ms</text>
</g>
<!-- ==================== RESOURCE BARS ==================== -->
<g transform="translate(320, 580)" class="resource-bars"
hx-get="/api/monitoring/resources/bars"
hx-trigger="load, every 15s"
hx-swap="innerHTML">
<!-- CPU -->
<g transform="translate(0, 0)">
<text x="0" y="12" fill="#94a3b8" font-family="system-ui" font-size="10" font-weight="500">CPU</text>
<rect x="40" y="2" width="100" height="14" rx="4" fill="#334155"/>
<rect x="40" y="2" width="65" height="14" rx="4" fill="url(#coreGlow)" class="resource-fill"/>
<text x="150" y="13" fill="#f8fafc" font-family="system-ui" font-size="11" font-weight="500">65%</text>
</g>
<!-- Memory -->
<g transform="translate(180, 0)">
<text x="0" y="12" fill="#94a3b8" font-family="system-ui" font-size="10" font-weight="500">MEM</text>
<rect x="40" y="2" width="100" height="14" rx="4" fill="#334155"/>
<rect x="40" y="2" width="72" height="14" rx="4" fill="url(#greenGlow)" class="resource-fill"/>
<text x="150" y="13" fill="#f8fafc" font-family="system-ui" font-size="11" font-weight="500">72%</text>
</g>
<!-- GPU -->
<g transform="translate(360, 0)">
<text x="0" y="12" fill="#94a3b8" font-family="system-ui" font-size="10" font-weight="500">GPU</text>
<rect x="40" y="2" width="100" height="14" rx="4" fill="#334155"/>
<rect x="40" y="2" width="45" height="14" rx="4" fill="url(#vectorGradient)" class="resource-fill"/>
<text x="150" y="13" fill="#f8fafc" font-family="system-ui" font-size="11" font-weight="500">45%</text>
</g>
<!-- Disk -->
<g transform="translate(540, 0)">
<text x="0" y="12" fill="#94a3b8" font-family="system-ui" font-size="10" font-weight="500">DISK</text>
<rect x="40" y="2" width="100" height="14" rx="4" fill="#334155"/>
<rect x="40" y="2" width="28" height="14" rx="4" fill="url(#storageGradient)" class="resource-fill"/>
<text x="150" y="13" fill="#f8fafc" font-family="system-ui" font-size="11" font-weight="500">28%</text>
</g>
</g>
<!-- ==================== ACTIVITY TICKER ==================== -->
<g transform="translate(320, 630)">
<rect x="0" y="0" width="520" height="40" rx="8" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<circle cx="18" cy="20" r="5" fill="#10b981" class="pulse-dot"/>
<text x="34" y="25" fill="#94a3b8" font-family="system-ui" font-size="12"
hx-get="/api/monitoring/activity/latest"
hx-trigger="load, every 5s"
hx-swap="innerHTML">System monitoring active...</text>
</g>
<!-- ==================== TITLE ==================== -->
<g transform="translate(600, 45)">
<text text-anchor="middle" fill="#f8fafc" font-family="system-ui" font-size="22" font-weight="600">Live System Monitor</text>
<text y="24" text-anchor="middle" fill="#64748b" font-family="system-ui" font-size="12"
hx-get="/api/monitoring/timestamp"
hx-trigger="load, every 5s"
hx-swap="innerHTML">Last updated: --</text>
</g>
<!-- ==================== LEGEND ==================== -->
<g transform="translate(50, 650)">
<g transform="translate(0, 0)">
<circle r="5" fill="#10b981"/>
<text x="12" y="4" fill="#94a3b8" font-family="system-ui" font-size="10">Running</text>
</g>
<g transform="translate(80, 0)">
<circle r="5" fill="#f59e0b"/>
<text x="12" y="4" fill="#94a3b8" font-family="system-ui" font-size="10">Warning</text>
</g>
<g transform="translate(160, 0)">
<circle r="5" fill="#ef4444"/>
<text x="12" y="4" fill="#94a3b8" font-family="system-ui" font-size="10">Stopped</text>
</g>
</g>
</svg>
</div>
<!-- Hidden service status updater -->
<div id="service-status-container" style="display: none;"
hx-get="/api/monitoring/services"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
</div>
<style>
.monitoring-home {
width: 100%;
height: 100%;
min-height: 600px;
background: var(--bg-dark, #0f172a);
}
.live-visualization {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.live-svg {
width: 100%;
height: auto;
max-height: 100vh;
}
/* Animations */
@keyframes rotate-slow {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes pulse-ring {
0%, 100% { r: 68; opacity: 0.6; }
50% { r: 72; opacity: 0.8; }
}
@keyframes pulse-dot {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.8); }
}
@keyframes ai-pulse {
0%, 100% { r: 5; }
50% { r: 7; }
}
@keyframes antenna-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.rotate-slow {
animation: rotate-slow 30s linear infinite;
transform-origin: center;
}
.pulse-ring {
animation: pulse-ring 3s ease-in-out infinite;
}
.pulse-dot {
animation: pulse-dot 1.5s ease-in-out infinite;
}
.ai-pulse {
animation: ai-pulse 1.5s ease-in-out infinite;
}
.antenna-pulse {
animation: antenna-pulse 1.5s ease-in-out infinite;
}
/* Status dots */
.status-dot {
transition: fill 0.3s ease;
}
.status-dot.running {
fill: #10b981;
animation: pulse-dot 2s ease-in-out infinite;
}
.status-dot.warning {
fill: #f59e0b;
animation: pulse-dot 1s ease-in-out infinite;
}
.status-dot.stopped {
fill: #ef4444;
animation: none;
}
/* Data packets */
.data-packet {
filter: url(#glow);
}
/* Connection lines on hover */
.service-node {
cursor: pointer;
transition: transform 0.2s ease;
}
.service-node:hover {
transform: scale(1.05);
}
.service-node:hover circle:first-child {
stroke-width: 3;
}
/* Resource bars animation */
.resource-fill {
transition: width 0.5s ease-out;
}
/* Responsive */
@media (max-width: 1200px) {
.live-svg {
min-width: 1000px;
}
.live-visualization {
overflow-x: auto;
}
}
/* Dark mode default, light mode adjustments */
@media (prefers-color-scheme: light) {
.monitoring-home {
--bg-dark: #f8fafc;
--bg-surface: #e2e8f0;
}
}
</style>
<script>
// Update service status dots based on API response
document.body.addEventListener('htmx:afterSwap', function(event) {
if (event.detail.target.id === 'service-status-container') {
try {
const data = JSON.parse(event.detail.target.textContent);
Object.entries(data).forEach(([service, status]) => {
const dot = document.querySelector(`[data-status="${service}"]`);
if (dot) {
dot.classList.remove('running', 'warning', 'stopped');
dot.classList.add(status);
}
});
} catch (e) {
console.log('Service status update:', e);
}
}
});
// Add click handlers for service nodes
document.querySelectorAll('.service-node').forEach(node => {
node.addEventListener('click', function() {
const service = this.dataset.service;
if (service) {
// Navigate to detailed service view
htmx.ajax('GET', `/monitoring/service/${service}`, {target: '#main-content'});
}
});
});
// Keyboard shortcut: R to refresh
document.addEventListener('keydown', function(e) {
if (e.key === 'r' && !e.ctrlKey && !e.metaKey) {
htmx.trigger(document.body, 'htmx:load');
}
});
</script>
</div>

View file

@ -0,0 +1,376 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 700" width="100%" height="100%">
<defs>
<!-- Gradients -->
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#0f172a"/>
<stop offset="50%" style="stop-color:#1e293b"/>
<stop offset="100%" style="stop-color:#0f172a"/>
</linearGradient>
<linearGradient id="coreGlow" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3b82f6"/>
<stop offset="50%" style="stop-color:#6366f1"/>
<stop offset="100%" style="stop-color:#8b5cf6"/>
</linearGradient>
<linearGradient id="greenGlow" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#10b981"/>
<stop offset="100%" style="stop-color:#22c55e"/>
</linearGradient>
<linearGradient id="amberGlow" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#f59e0b"/>
<stop offset="100%" style="stop-color:#fbbf24"/>
</linearGradient>
<linearGradient id="redGlow" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#ef4444"/>
<stop offset="100%" style="stop-color:#f87171"/>
</linearGradient>
<linearGradient id="dbGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3b82f6"/>
<stop offset="100%" style="stop-color:#1d4ed8"/>
</linearGradient>
<linearGradient id="vectorGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#8b5cf6"/>
<stop offset="100%" style="stop-color:#6d28d9"/>
</linearGradient>
<linearGradient id="storageGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#f59e0b"/>
<stop offset="100%" style="stop-color:#d97706"/>
</linearGradient>
<linearGradient id="aiGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#ec4899"/>
<stop offset="100%" style="stop-color:#db2777"/>
</linearGradient>
<linearGradient id="cacheGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#06b6d4"/>
<stop offset="100%" style="stop-color:#0891b2"/>
</linearGradient>
<!-- Filters -->
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="softGlow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="6" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<!-- Animated pulse -->
<circle id="pulseTemplate" r="4">
<animate attributeName="r" values="4;8;4" dur="2s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="1;0.3;1" dur="2s" repeatCount="indefinite"/>
</circle>
<!-- Data packet for animation -->
<circle id="dataPacket" r="3" fill="#60a5fa">
<animate attributeName="opacity" values="1;0.6;1" dur="0.5s" repeatCount="indefinite"/>
</circle>
</defs>
<!-- Background -->
<rect width="100%" height="100%" fill="url(#bgGradient)"/>
<!-- Grid pattern -->
<g opacity="0.1" stroke="#64748b" stroke-width="0.5">
<line x1="0" y1="175" x2="1200" y2="175"/>
<line x1="0" y1="350" x2="1200" y2="350"/>
<line x1="0" y1="525" x2="1200" y2="525"/>
<line x1="300" y1="0" x2="300" y2="700"/>
<line x1="600" y1="0" x2="600" y2="700"/>
<line x1="900" y1="0" x2="900" y2="700"/>
</g>
<!-- ==================== CENTRAL CORE: BotServer ==================== -->
<g transform="translate(600, 350)">
<!-- Outer rotating ring -->
<circle r="90" fill="none" stroke="url(#coreGlow)" stroke-width="2" stroke-dasharray="10 5" opacity="0.4">
<animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="30s" repeatCount="indefinite"/>
</circle>
<!-- Middle pulsing ring -->
<circle r="70" fill="none" stroke="url(#coreGlow)" stroke-width="3" opacity="0.6">
<animate attributeName="r" values="68;72;68" dur="3s" repeatCount="indefinite"/>
</circle>
<!-- Core background -->
<circle r="55" fill="#1e293b" stroke="url(#coreGlow)" stroke-width="3" filter="url(#glow)"/>
<!-- Core icon - Bot -->
<g fill="#f8fafc">
<rect x="-25" y="-20" width="50" height="35" rx="8" fill="url(#coreGlow)" opacity="0.9"/>
<circle cx="-10" cy="-5" r="5" fill="#f8fafc"/>
<circle cx="10" cy="-5" r="5" fill="#f8fafc"/>
<path d="M-12 10 Q0 18 12 10" stroke="#f8fafc" stroke-width="2.5" fill="none"/>
<rect x="-3" y="-35" width="6" height="12" rx="3" fill="url(#coreGlow)"/>
<circle cx="0" cy="-38" r="5" fill="url(#coreGlow)">
<animate attributeName="opacity" values="1;0.5;1" dur="1.5s" repeatCount="indefinite"/>
</circle>
</g>
<!-- Label -->
<text y="80" text-anchor="middle" fill="#f8fafc" font-family="system-ui" font-size="14" font-weight="600">BotServer</text>
<text y="98" text-anchor="middle" fill="#94a3b8" font-family="system-ui" font-size="11" id="core-status">● Running</text>
</g>
<!-- ==================== LEFT SIDE: Data Layer ==================== -->
<!-- PostgreSQL -->
<g transform="translate(150, 200)">
<circle r="45" fill="#1e293b" stroke="url(#dbGradient)" stroke-width="2" filter="url(#glow)"/>
<g transform="translate(-15, -18)">
<ellipse cx="15" cy="0" rx="18" ry="6" fill="url(#dbGradient)"/>
<path d="M-3 0 L-3 20 Q15 30 33 20 L33 0" fill="url(#dbGradient)" opacity="0.8"/>
<ellipse cx="15" cy="20" rx="18" ry="6" fill="url(#dbGradient)"/>
<line x1="-3" y1="7" x2="33" y2="7" stroke="#1e40af" stroke-width="1"/>
<line x1="-3" y1="14" x2="33" y2="14" stroke="#1e40af" stroke-width="1"/>
</g>
<text y="65" text-anchor="middle" fill="#f8fafc" font-family="system-ui" font-size="12" font-weight="500">PostgreSQL</text>
<circle cx="35" cy="-35" r="6" fill="#10b981" id="pg-status">
<animate attributeName="opacity" values="1;0.6;1" dur="2s" repeatCount="indefinite"/>
</circle>
</g>
<!-- Qdrant -->
<g transform="translate(150, 350)">
<circle r="45" fill="#1e293b" stroke="url(#vectorGradient)" stroke-width="2" filter="url(#glow)"/>
<g fill="url(#vectorGradient)">
<polygon points="0,-22 19,11 -19,11" opacity="0.9"/>
<polygon points="0,-12 12,7 -12,7" fill="#1e293b"/>
<circle cx="0" cy="-22" r="4"/>
<circle cx="19" cy="11" r="4"/>
<circle cx="-19" cy="11" r="4"/>
<line x1="0" y1="-18" x2="0" y2="-8" stroke="#c4b5fd" stroke-width="1.5"/>
<line x1="15" y1="9" x2="8" y2="5" stroke="#c4b5fd" stroke-width="1.5"/>
<line x1="-15" y1="9" x2="-8" y2="5" stroke="#c4b5fd" stroke-width="1.5"/>
</g>
<text y="65" text-anchor="middle" fill="#f8fafc" font-family="system-ui" font-size="12" font-weight="500">Qdrant</text>
<circle cx="35" cy="-35" r="6" fill="#10b981" id="qdrant-status">
<animate attributeName="opacity" values="1;0.6;1" dur="2s" repeatCount="indefinite"/>
</circle>
</g>
<!-- MinIO -->
<g transform="translate(150, 500)">
<circle r="45" fill="#1e293b" stroke="url(#storageGradient)" stroke-width="2" filter="url(#glow)"/>
<g fill="url(#storageGradient)">
<rect x="-22" y="-18" width="44" height="36" rx="4"/>
<rect x="-18" y="-14" width="36" height="28" rx="2" fill="#1e293b"/>
<rect x="-14" y="-10" width="28" height="8" rx="1" fill="#fcd34d" opacity="0.4"/>
<rect x="-14" y="2" width="28" height="8" rx="1" fill="#fcd34d" opacity="0.4"/>
<circle cx="10" cy="-6" r="2" fill="#fbbf24"/>
<circle cx="10" cy="6" r="2" fill="#fbbf24"/>
</g>
<text y="65" text-anchor="middle" fill="#f8fafc" font-family="system-ui" font-size="12" font-weight="500">MinIO</text>
<circle cx="35" cy="-35" r="6" fill="#10b981" id="minio-status">
<animate attributeName="opacity" values="1;0.6;1" dur="2s" repeatCount="indefinite"/>
</circle>
</g>
<!-- ==================== RIGHT SIDE: Services ==================== -->
<!-- BotModels -->
<g transform="translate(1050, 200)">
<circle r="45" fill="#1e293b" stroke="url(#aiGradient)" stroke-width="2" filter="url(#glow)"/>
<g>
<circle cx="0" cy="-8" r="14" fill="none" stroke="url(#aiGradient)" stroke-width="2"/>
<circle cx="0" cy="-8" r="6" fill="url(#aiGradient)">
<animate attributeName="r" values="5;7;5" dur="1.5s" repeatCount="indefinite"/>
</circle>
<path d="M-18 12 L0 -2 L18 12" stroke="url(#aiGradient)" stroke-width="2" fill="none"/>
<path d="M-12 18 L0 8 L12 18" stroke="url(#aiGradient)" stroke-width="2" fill="none"/>
</g>
<text y="65" text-anchor="middle" fill="#f8fafc" font-family="system-ui" font-size="12" font-weight="500">BotModels</text>
<circle cx="35" cy="-35" r="6" fill="#10b981" id="botmodels-status">
<animate attributeName="opacity" values="1;0.6;1" dur="2s" repeatCount="indefinite"/>
</circle>
</g>
<!-- Cache -->
<g transform="translate(1050, 350)">
<circle r="45" fill="#1e293b" stroke="url(#cacheGradient)" stroke-width="2" filter="url(#glow)"/>
<g fill="url(#cacheGradient)">
<rect x="-20" y="-18" width="40" height="36" rx="3"/>
<rect x="-16" y="-14" width="32" height="28" rx="2" fill="#1e293b"/>
<text x="0" y="2" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#22d3ee"></text>
</g>
<text y="65" text-anchor="middle" fill="#f8fafc" font-family="system-ui" font-size="12" font-weight="500">Cache</text>
<circle cx="35" cy="-35" r="6" fill="#10b981" id="cache-status">
<animate attributeName="opacity" values="1;0.6;1" dur="2s" repeatCount="indefinite"/>
</circle>
</g>
<!-- Vault -->
<g transform="translate(1050, 500)">
<circle r="45" fill="#1e293b" stroke="url(#greenGlow)" stroke-width="2" filter="url(#glow)"/>
<g fill="url(#greenGlow)">
<rect x="-18" y="-10" width="36" height="28" rx="4"/>
<rect x="-14" y="-6" width="28" height="20" rx="2" fill="#1e293b"/>
<circle cx="0" cy="4" r="6" fill="url(#greenGlow)"/>
<rect x="-2" y="4" width="4" height="8" fill="url(#greenGlow)"/>
<path d="M-10 -18 L0 -26 L10 -18" stroke="url(#greenGlow)" stroke-width="3" fill="none"/>
</g>
<text y="65" text-anchor="middle" fill="#f8fafc" font-family="system-ui" font-size="12" font-weight="500">Vault</text>
<circle cx="35" cy="-35" r="6" fill="#10b981" id="vault-status">
<animate attributeName="opacity" values="1;0.6;1" dur="2s" repeatCount="indefinite"/>
</circle>
</g>
<!-- ==================== CONNECTION LINES ==================== -->
<!-- Left connections to core -->
<g stroke-width="2" fill="none">
<!-- PostgreSQL to Core -->
<path d="M195 200 Q400 200 505 320" stroke="#3b82f6" opacity="0.4"/>
<circle r="4" fill="#60a5fa">
<animateMotion dur="3s" repeatCount="indefinite" path="M195 200 Q400 200 505 320"/>
</circle>
<!-- Qdrant to Core -->
<path d="M195 350 L505 350" stroke="#8b5cf6" opacity="0.4"/>
<circle r="4" fill="#a78bfa">
<animateMotion dur="2.5s" repeatCount="indefinite" path="M195 350 L505 350"/>
</circle>
<!-- MinIO to Core -->
<path d="M195 500 Q400 500 505 380" stroke="#f59e0b" opacity="0.4"/>
<circle r="4" fill="#fcd34d">
<animateMotion dur="3.5s" repeatCount="indefinite" path="M195 500 Q400 500 505 380"/>
</circle>
</g>
<!-- Right connections to core -->
<g stroke-width="2" fill="none">
<!-- Core to BotModels -->
<path d="M695 320 Q800 200 1005 200" stroke="#ec4899" opacity="0.4"/>
<circle r="4" fill="#f472b6">
<animateMotion dur="2s" repeatCount="indefinite" path="M695 320 Q800 200 1005 200"/>
</circle>
<!-- Core to Cache -->
<path d="M695 350 L1005 350" stroke="#06b6d4" opacity="0.4"/>
<circle r="4" fill="#22d3ee">
<animateMotion dur="1.5s" repeatCount="indefinite" path="M695 350 L1005 350"/>
</circle>
<!-- Core to Vault -->
<path d="M695 380 Q800 500 1005 500" stroke="#10b981" opacity="0.4"/>
<circle r="4" fill="#34d399">
<animateMotion dur="4s" repeatCount="indefinite" path="M695 380 Q800 500 1005 500"/>
</circle>
</g>
<!-- ==================== METRICS PANELS ==================== -->
<!-- Sessions Panel -->
<g transform="translate(320, 100)">
<rect x="0" y="0" width="160" height="80" rx="8" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="12" y="24" fill="#94a3b8" font-family="system-ui" font-size="11">ACTIVE SESSIONS</text>
<text x="12" y="55" fill="#f8fafc" font-family="system-ui" font-size="28" font-weight="700" id="sessions-count">--</text>
<text x="148" y="55" fill="#10b981" font-family="system-ui" font-size="11" text-anchor="end" id="sessions-trend">--</text>
</g>
<!-- Messages Panel -->
<g transform="translate(500, 100)">
<rect x="0" y="0" width="160" height="80" rx="8" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="12" y="24" fill="#94a3b8" font-family="system-ui" font-size="11">MESSAGES TODAY</text>
<text x="12" y="55" fill="#f8fafc" font-family="system-ui" font-size="28" font-weight="700" id="messages-count">--</text>
<text x="148" y="55" fill="#94a3b8" font-family="system-ui" font-size="11" text-anchor="end" id="messages-rate">--/hr</text>
</g>
<!-- Response Time Panel -->
<g transform="translate(680, 100)">
<rect x="0" y="0" width="160" height="80" rx="8" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="12" y="24" fill="#94a3b8" font-family="system-ui" font-size="11">AVG RESPONSE</text>
<text x="12" y="55" fill="#f8fafc" font-family="system-ui" font-size="28" font-weight="700" id="response-time">--</text>
<text x="148" y="55" fill="#94a3b8" font-family="system-ui" font-size="11" text-anchor="end">ms</text>
</g>
<!-- ==================== RESOURCE BARS ==================== -->
<g transform="translate(320, 580)">
<!-- CPU -->
<g transform="translate(0, 0)">
<text x="0" y="12" fill="#94a3b8" font-family="system-ui" font-size="10">CPU</text>
<rect x="40" y="2" width="100" height="12" rx="3" fill="#334155"/>
<rect x="40" y="2" width="65" height="12" rx="3" fill="url(#coreGlow)" id="cpu-bar">
<animate attributeName="width" values="60;70;65" dur="5s" repeatCount="indefinite"/>
</rect>
<text x="148" y="12" fill="#f8fafc" font-family="system-ui" font-size="10" id="cpu-pct">65%</text>
</g>
<!-- Memory -->
<g transform="translate(180, 0)">
<text x="0" y="12" fill="#94a3b8" font-family="system-ui" font-size="10">MEM</text>
<rect x="40" y="2" width="100" height="12" rx="3" fill="#334155"/>
<rect x="40" y="2" width="72" height="12" rx="3" fill="url(#greenGlow)" id="mem-bar"/>
<text x="148" y="12" fill="#f8fafc" font-family="system-ui" font-size="10" id="mem-pct">72%</text>
</g>
<!-- GPU -->
<g transform="translate(360, 0)">
<text x="0" y="12" fill="#94a3b8" font-family="system-ui" font-size="10">GPU</text>
<rect x="40" y="2" width="100" height="12" rx="3" fill="#334155"/>
<rect x="40" y="2" width="45" height="12" rx="3" fill="url(#vectorGradient)" id="gpu-bar"/>
<text x="148" y="12" fill="#f8fafc" font-family="system-ui" font-size="10" id="gpu-pct">45%</text>
</g>
<!-- Disk -->
<g transform="translate(540, 0)">
<text x="0" y="12" fill="#94a3b8" font-family="system-ui" font-size="10">DISK</text>
<rect x="40" y="2" width="100" height="12" rx="3" fill="#334155"/>
<rect x="40" y="2" width="28" height="12" rx="3" fill="url(#storageGradient)" id="disk-bar"/>
<text x="148" y="12" fill="#f8fafc" font-family="system-ui" font-size="10" id="disk-pct">28%</text>
</g>
</g>
<!-- ==================== LIVE ACTIVITY TICKER ==================== -->
<g transform="translate(320, 640)">
<rect x="0" y="0" width="520" height="36" rx="6" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<circle cx="16" cy="18" r="4" fill="#10b981">
<animate attributeName="opacity" values="1;0.3;1" dur="1s" repeatCount="indefinite"/>
</circle>
<text x="30" y="23" fill="#94a3b8" font-family="system-ui" font-size="12" id="activity-text">Monitoring active...</text>
</g>
<!-- ==================== TITLE ==================== -->
<g transform="translate(600, 50)">
<text text-anchor="middle" fill="#f8fafc" font-family="system-ui" font-size="20" font-weight="600">Live System Monitor</text>
<text y="22" text-anchor="middle" fill="#64748b" font-family="system-ui" font-size="12" id="last-updated">Last updated: --</text>
</g>
<!-- ==================== LEGEND ==================== -->
<g transform="translate(50, 630)">
<g transform="translate(0, 0)">
<circle r="5" fill="#10b981"/>
<text x="12" y="4" fill="#94a3b8" font-family="system-ui" font-size="10">Running</text>
</g>
<g transform="translate(70, 0)">
<circle r="5" fill="#f59e0b"/>
<text x="12" y="4" fill="#94a3b8" font-family="system-ui" font-size="10">Warning</text>
</g>
<g transform="translate(140, 0)">
<circle r="5" fill="#ef4444"/>
<text x="12" y="4" fill="#94a3b8" font-family="system-ui" font-size="10">Stopped</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB