botui/ui/suite/monitoring/monitoring.html

1722 lines
53 KiB
HTML

<div class="monitoring-container" id="monitoring-app">
<header class="monitoring-header">
<h2>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
class="header-icon"
>
<circle
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="1.</h2>5"
fill="none"
opacity="0.3"
/>
<circle
cx="12"
cy="12"
r="6.5"
stroke="currentColor"
stroke-width="1.5"
fill="none"
opacity="0.6"
/>
<circle cx="12" cy="12" r="2" fill="currentColor" />
<line
x1="12"
y1="2"
x2="12"
y2="5"
stroke="currentColor"
stroke-width="1.5"
/>
<line
x1="12"
y1="19"
x2="12"
y2="22"
stroke="currentColor"
stroke-width="1.5"
/>
<line
x1="2"
y1="12"
x2="5"
y2="12"
stroke="currentColor"
stroke-width="1.5"
/>
<line
x1="19"
y1="12"
x2="22"
y2="12"
stroke="currentColor"
stroke-width="1.5"
/>
</svg>
Monitoring Dashboard
</h2>
<div class="header-actions">
<button class="view-toggle" id="view-toggle" title="Toggle View">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="3" width="7" height="7" />
<rect x="14" y="3" width="7" height="7" />
<rect x="3" y="14" width="7" height="7" />
<rect x="14" y="14" width="7" height="7" />
</svg>
</button>
<span
class="last-updated"
hx-get="/api/monitoring/timestamp"
hx-trigger="load, every 5s"
hx-swap="innerHTML"
>--</span
>
</div>
</header>
<!-- Live System Visualization -->
<div class="live-visualization" id="live-view">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1200 650"
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="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>
<!-- Glow filter -->
<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>
</defs>
<!-- Background -->
<rect width="100%" height="100%" fill="url(#bgGradient)" />
<!-- Subtle grid -->
<g opacity="0.06" stroke="#64748b" stroke-width="0.5">
<line x1="0" y1="162" x2="1200" y2="162" />
<line x1="0" y1="325" x2="1200" y2="325" />
<line x1="0" y1="487" x2="1200" y2="487" />
<line x1="300" y1="0" x2="300" y2="650" />
<line x1="600" y1="0" x2="600" y2="650" />
<line x1="900" y1="0" x2="900" y2="650" />
</g>
<!-- ==================== CENTRAL CORE: BotServer ==================== -->
<g transform="translate(600, 325)" class="core-node">
<!-- Outer rotating ring -->
<circle
r="85"
fill="none"
stroke="url(#coreGlow)"
stroke-width="2"
stroke-dasharray="10 5"
opacity="0.4"
class="rotate-slow"
/>
<!-- Middle pulsing ring -->
<circle
r="65"
fill="none"
stroke="url(#coreGlow)"
stroke-width="3"
opacity="0.6"
class="pulse-ring"
/>
<!-- Core background -->
<circle
r="50"
fill="#1e293b"
stroke="url(#coreGlow)"
stroke-width="3"
filter="url(#glow)"
/>
<!-- Bot icon -->
<g fill="#f8fafc">
<rect
x="-22"
y="-18"
width="44"
height="32"
rx="7"
fill="url(#coreGlow)"
opacity="0.9"
/>
<circle cx="-9" cy="-4" r="4.5" fill="#f8fafc" />
<circle cx="9" cy="-4" r="4.5" fill="#f8fafc" />
<path
d="M-10 9 Q0 16 10 9"
stroke="#f8fafc"
stroke-width="2.5"
fill="none"
/>
<rect
x="-2.5"
y="-32"
width="5"
height="11"
rx="2.5"
fill="url(#coreGlow)"
/>
<circle
cx="0"
cy="-35"
r="4.5"
fill="url(#coreGlow)"
class="antenna-pulse"
/>
</g>
<text
y="72"
text-anchor="middle"
fill="#f8fafc"
font-family="system-ui"
font-size="13"
font-weight="600"
>
BotServer
</text>
<text
y="88"
text-anchor="middle"
fill="#10b981"
font-family="system-ui"
font-size="10"
class="status-text"
>
● Running
</text>
</g>
<!-- ==================== LEFT: Data Layer ==================== -->
<!-- PostgreSQL -->
<g
transform="translate(150, 185)"
class="service-node"
data-service="postgresql"
>
<circle
r="42"
fill="#1e293b"
stroke="url(#dbGradient)"
stroke-width="2"
filter="url(#glow)"
/>
<g transform="translate(-14, -16)">
<ellipse
cx="14"
cy="0"
rx="16"
ry="5.5"
fill="url(#dbGradient)"
/>
<path
d="M-2 0 L-2 18 Q14 27 30 18 L30 0"
fill="url(#dbGradient)"
opacity="0.8"
/>
<ellipse
cx="14"
cy="18"
rx="16"
ry="5.5"
fill="url(#dbGradient)"
/>
<line
x1="-2"
y1="6"
x2="30"
y2="6"
stroke="#1e40af"
stroke-width="1"
/>
<line
x1="-2"
y1="12"
x2="30"
y2="12"
stroke="#1e40af"
stroke-width="1"
/>
</g>
<text
y="58"
text-anchor="middle"
fill="#f8fafc"
font-family="system-ui"
font-size="11"
font-weight="500"
>
PostgreSQL
</text>
<circle
cx="32"
cy="-32"
r="5"
class="status-dot running"
data-status="postgresql"
/>
</g>
<!-- Qdrant -->
<g
transform="translate(150, 325)"
class="service-node"
data-service="qdrant"
>
<circle
r="42"
fill="#1e293b"
stroke="url(#vectorGradient)"
stroke-width="2"
filter="url(#glow)"
/>
<g fill="url(#vectorGradient)">
<polygon points="0,-20 17,10 -17,10" opacity="0.9" />
<polygon points="0,-11 11,6 -11,6" fill="#1e293b" />
<circle cx="0" cy="-20" r="3.5" />
<circle cx="17" cy="10" r="3.5" />
<circle cx="-17" cy="10" r="3.5" />
</g>
<text
y="58"
text-anchor="middle"
fill="#f8fafc"
font-family="system-ui"
font-size="11"
font-weight="500"
>
Qdrant
</text>
<circle
cx="32"
cy="-32"
r="5"
class="status-dot running"
data-status="qdrant"
/>
</g>
<!-- MinIO -->
<g
transform="translate(150, 465)"
class="service-node"
data-service="drive"
>
<circle
r="42"
fill="#1e293b"
stroke="url(#storageGradient)"
stroke-width="2"
filter="url(#glow)"
/>
<g fill="url(#storageGradient)">
<rect x="-20" y="-16" width="40" height="32" rx="4" />
<rect
x="-16"
y="-12"
width="32"
height="24"
rx="2"
fill="#1e293b"
/>
<rect
x="-12"
y="-8"
width="24"
height="7"
rx="1"
fill="#fcd34d"
opacity="0.4"
/>
<rect
x="-12"
y="2"
width="24"
height="7"
rx="1"
fill="#fcd34d"
opacity="0.4"
/>
<circle cx="8" cy="-4.5" r="1.8" fill="#fbbf24" />
<circle cx="8" cy="5.5" r="1.8" fill="#fbbf24" />
</g>
<text
y="58"
text-anchor="middle"
fill="#f8fafc"
font-family="system-ui"
font-size="11"
font-weight="500"
>
MinIO
</text>
<circle
cx="32"
cy="-32"
r="5"
class="status-dot running"
data-status="drive"
/>
</g>
<!-- ==================== RIGHT: Services ==================== -->
<!-- BotModels -->
<g
transform="translate(1050, 185)"
class="service-node"
data-service="botmodels"
>
<circle
r="42"
fill="#1e293b"
stroke="url(#aiGradient)"
stroke-width="2"
filter="url(#glow)"
/>
<g>
<circle
cx="0"
cy="-7"
r="12"
fill="none"
stroke="url(#aiGradient)"
stroke-width="2"
/>
<circle
cx="0"
cy="-7"
r="5"
fill="url(#aiGradient)"
class="ai-pulse"
/>
<path
d="M-16 10 L0 -2 L16 10"
stroke="url(#aiGradient)"
stroke-width="2"
fill="none"
/>
<path
d="M-10 16 L0 7 L10 16"
stroke="url(#aiGradient)"
stroke-width="2"
fill="none"
/>
</g>
<text
y="58"
text-anchor="middle"
fill="#f8fafc"
font-family="system-ui"
font-size="11"
font-weight="500"
>
BotModels
</text>
<circle
cx="32"
cy="-32"
r="5"
class="status-dot running"
data-status="botmodels"
/>
</g>
<!-- Cache -->
<g
transform="translate(1050, 325)"
class="service-node"
data-service="cache"
>
<circle
r="42"
fill="#1e293b"
stroke="url(#cacheGradient)"
stroke-width="2"
filter="url(#glow)"
/>
<g fill="url(#cacheGradient)">
<rect x="-18" y="-16" width="36" height="32" rx="3" />
<rect
x="-14"
y="-12"
width="28"
height="24"
rx="2"
fill="#1e293b"
/>
<text
x="0"
y="5"
text-anchor="middle"
font-family="system-ui"
font-size="16"
fill="#22d3ee"
>
</text>
</g>
<text
y="58"
text-anchor="middle"
fill="#f8fafc"
font-family="system-ui"
font-size="11"
font-weight="500"
>
Cache
</text>
<circle
cx="32"
cy="-32"
r="5"
class="status-dot running"
data-status="cache"
/>
</g>
<!-- Vault -->
<g
transform="translate(1050, 465)"
class="service-node"
data-service="vault"
>
<circle
r="42"
fill="#1e293b"
stroke="url(#greenGlow)"
stroke-width="2"
filter="url(#glow)"
/>
<g fill="url(#greenGlow)">
<rect x="-16" y="-9" width="32" height="25" rx="4" />
<rect
x="-12"
y="-5"
width="24"
height="17"
rx="2"
fill="#1e293b"
/>
<circle cx="0" cy="3.5" r="5" fill="url(#greenGlow)" />
<rect
x="-1.5"
y="3.5"
width="3"
height="7"
fill="url(#greenGlow)"
/>
<path
d="M-9 -16 L0 -23 L9 -16"
stroke="url(#greenGlow)"
stroke-width="2.5"
fill="none"
/>
</g>
<text
y="58"
text-anchor="middle"
fill="#f8fafc"
font-family="system-ui"
font-size="11"
font-weight="500"
>
Vault
</text>
<circle
cx="32"
cy="-32"
r="5"
class="status-dot running"
data-status="vault"
/>
</g>
<!-- ==================== ANIMATED CONNECTION LINES ==================== -->
<!-- Left connections -->
<g class="connection-lines" stroke-width="2" fill="none">
<path
d="M192 185 Q400 185 510 300"
stroke="#3b82f6"
opacity="0.25"
class="connection-path"
/>
<circle r="3.5" fill="#60a5fa" class="data-packet">
<animateMotion
dur="3s"
repeatCount="indefinite"
path="M192 185 Q400 185 510 300"
/>
</circle>
<path
d="M192 325 L510 325"
stroke="#8b5cf6"
opacity="0.25"
class="connection-path"
/>
<circle r="3.5" fill="#a78bfa" class="data-packet">
<animateMotion
dur="2.5s"
repeatCount="indefinite"
path="M192 325 L510 325"
/>
</circle>
<path
d="M192 465 Q400 465 510 350"
stroke="#f59e0b"
opacity="0.25"
class="connection-path"
/>
<circle r="3.5" fill="#fcd34d" class="data-packet">
<animateMotion
dur="3.5s"
repeatCount="indefinite"
path="M192 465 Q400 465 510 350"
/>
</circle>
</g>
<!-- Right connections -->
<g class="connection-lines" stroke-width="2" fill="none">
<path
d="M690 300 Q800 185 1008 185"
stroke="#ec4899"
opacity="0.25"
class="connection-path"
/>
<circle r="3.5" fill="#f472b6" class="data-packet">
<animateMotion
dur="2s"
repeatCount="indefinite"
path="M690 300 Q800 185 1008 185"
/>
</circle>
<path
d="M690 325 L1008 325"
stroke="#06b6d4"
opacity="0.25"
class="connection-path"
/>
<circle r="3.5" fill="#22d3ee" class="data-packet">
<animateMotion
dur="1.5s"
repeatCount="indefinite"
path="M690 325 L1008 325"
/>
</circle>
<path
d="M690 350 Q800 465 1008 465"
stroke="#10b981"
opacity="0.25"
class="connection-path"
/>
<circle r="3.5" fill="#34d399" class="data-packet">
<animateMotion
dur="4s"
repeatCount="indefinite"
path="M690 350 Q800 465 1008 465"
/>
</circle>
</g>
<!-- ==================== METRICS PANELS ==================== -->
<!-- Sessions -->
<g transform="translate(330, 50)">
<rect
x="0"
y="0"
width="150"
height="75"
rx="10"
fill="#1e293b"
stroke="#334155"
stroke-width="1"
/>
<text
x="12"
y="22"
fill="#64748b"
font-family="system-ui"
font-size="9"
font-weight="500"
>
ACTIVE SESSIONS
</text>
<text
x="12"
y="52"
fill="#f8fafc"
font-family="system-ui"
font-size="28"
font-weight="700"
hx-get="/api/monitoring/metric/sessions"
hx-trigger="load, every 5s"
hx-swap="innerHTML"
>
--
</text>
<text
x="138"
y="62"
fill="#10b981"
font-family="system-ui"
font-size="10"
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, 50)">
<rect
x="0"
y="0"
width="150"
height="75"
rx="10"
fill="#1e293b"
stroke="#334155"
stroke-width="1"
/>
<text
x="12"
y="22"
fill="#64748b"
font-family="system-ui"
font-size="9"
font-weight="500"
>
MESSAGES TODAY
</text>
<text
x="12"
y="52"
fill="#f8fafc"
font-family="system-ui"
font-size="28"
font-weight="700"
hx-get="/api/monitoring/metric/messages"
hx-trigger="load, every 10s"
hx-swap="innerHTML"
>
--
</text>
<text
x="138"
y="62"
fill="#94a3b8"
font-family="system-ui"
font-size="10"
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(670, 50)">
<rect
x="0"
y="0"
width="150"
height="75"
rx="10"
fill="#1e293b"
stroke="#334155"
stroke-width="1"
/>
<text
x="12"
y="22"
fill="#64748b"
font-family="system-ui"
font-size="9"
font-weight="500"
>
AVG RESPONSE
</text>
<text
x="12"
y="52"
fill="#f8fafc"
font-family="system-ui"
font-size="28"
font-weight="700"
hx-get="/api/monitoring/metric/response_time"
hx-trigger="load, every 10s"
hx-swap="innerHTML"
>
--
</text>
<text
x="138"
y="62"
fill="#94a3b8"
font-family="system-ui"
font-size="10"
text-anchor="end"
>
ms
</text>
</g>
<!-- ==================== RESOURCE BARS ==================== -->
<g
transform="translate(330, 545)"
hx-get="/api/monitoring/resources/bars"
hx-trigger="load, every 15s"
hx-swap="innerHTML"
>
<!-- CPU -->
<g transform="translate(0, 0)">
<text
x="0"
y="11"
fill="#94a3b8"
font-family="system-ui"
font-size="9"
font-weight="500"
>
CPU
</text>
<rect
x="35"
y="2"
width="90"
height="12"
rx="4"
fill="#334155"
/>
<rect
x="35"
y="2"
width="58"
height="12"
rx="4"
fill="url(#coreGlow)"
class="resource-fill"
/>
<text
x="132"
y="12"
fill="#f8fafc"
font-family="system-ui"
font-size="10"
font-weight="500"
>
65%
</text>
</g>
<!-- Memory -->
<g transform="translate(160, 0)">
<text
x="0"
y="11"
fill="#94a3b8"
font-family="system-ui"
font-size="9"
font-weight="500"
>
MEM
</text>
<rect
x="35"
y="2"
width="90"
height="12"
rx="4"
fill="#334155"
/>
<rect
x="35"
y="2"
width="65"
height="12"
rx="4"
fill="url(#greenGlow)"
class="resource-fill"
/>
<text
x="132"
y="12"
fill="#f8fafc"
font-family="system-ui"
font-size="10"
font-weight="500"
>
72%
</text>
</g>
<!-- GPU -->
<g transform="translate(320, 0)">
<text
x="0"
y="11"
fill="#94a3b8"
font-family="system-ui"
font-size="9"
font-weight="500"
>
GPU
</text>
<rect
x="35"
y="2"
width="90"
height="12"
rx="4"
fill="#334155"
/>
<rect
x="35"
y="2"
width="40"
height="12"
rx="4"
fill="url(#vectorGradient)"
class="resource-fill"
/>
<text
x="132"
y="12"
fill="#f8fafc"
font-family="system-ui"
font-size="10"
font-weight="500"
>
45%
</text>
</g>
<!-- Disk -->
<g transform="translate(480, 0)">
<text
x="0"
y="11"
fill="#94a3b8"
font-family="system-ui"
font-size="9"
font-weight="500"
>
DISK
</text>
<rect
x="35"
y="2"
width="90"
height="12"
rx="4"
fill="#334155"
/>
<rect
x="35"
y="2"
width="25"
height="12"
rx="4"
fill="url(#storageGradient)"
class="resource-fill"
/>
<text
x="132"
y="12"
fill="#f8fafc"
font-family="system-ui"
font-size="10"
font-weight="500"
>
28%
</text>
</g>
</g>
<!-- ==================== ACTIVITY TICKER ==================== -->
<g transform="translate(330, 590)">
<rect
x="0"
y="0"
width="490"
height="36"
rx="8"
fill="#1e293b"
stroke="#334155"
stroke-width="1"
/>
<circle
cx="16"
cy="18"
r="4"
fill="#10b981"
class="pulse-dot"
/>
<text
x="30"
y="22"
fill="#94a3b8"
font-family="system-ui"
font-size="11"
hx-get="/api/monitoring/activity/latest"
hx-trigger="load, every 5s"
hx-swap="innerHTML"
>
System monitoring active...
</text>
</g>
<!-- ==================== LEGEND ==================== -->
<g transform="translate(50, 600)">
<g transform="translate(0, 0)">
<circle r="4" fill="#10b981" />
<text
x="10"
y="4"
fill="#94a3b8"
font-family="system-ui"
font-size="9"
>
Running
</text>
</g>
<g transform="translate(70, 0)">
<circle r="4" fill="#f59e0b" />
<text
x="10"
y="4"
fill="#94a3b8"
font-family="system-ui"
font-size="9"
>
Warning
</text>
</g>
<g transform="translate(140, 0)">
<circle r="4" fill="#ef4444" />
<text
x="10"
y="4"
fill="#94a3b8"
font-family="system-ui"
font-size="9"
>
Stopped
</text>
</g>
</g>
</svg>
</div>
<!-- Grid View (hidden by default) -->
<div class="monitoring-grid" id="grid-view" style="display: none">
<!-- Sessions Panel -->
<div
class="monitor-panel"
hx-get="/api/monitoring/sessions"
hx-trigger="load, every 5s"
hx-swap="innerHTML"
>
<div class="panel-header">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
class="panel-icon"
>
<circle
cx="12"
cy="8"
r="4"
stroke="currentColor"
stroke-width="1.5"
/>
<path
d="M4 20c0-4 4-6 8-6s8 2 8 6"
stroke="currentColor"
stroke-width="1.5"
/>
</svg>
<span>Sessions</span>
</div>
<div class="tree-view">
<div class="tree-node">
<span class="tree-branch"></span>
<span class="tree-label">Loading...</span>
</div>
</div>
</div>
<!-- Messages Panel -->
<div
class="monitor-panel"
hx-get="/api/monitoring/messages"
hx-trigger="load, every 10s"
hx-swap="innerHTML"
>
<div class="panel-header">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
class="panel-icon"
>
<path
d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
stroke="currentColor"
stroke-width="1.5"
/>
</svg>
<span>Messages</span>
</div>
<div class="tree-view">
<div class="tree-node">
<span class="tree-branch"></span>
<span class="tree-label">Loading...</span>
</div>
</div>
</div>
<!-- Resources Panel -->
<div
class="monitor-panel resources-panel"
hx-get="/api/monitoring/resources"
hx-trigger="load, every 15s"
hx-swap="innerHTML"
>
<div class="panel-header">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
class="panel-icon"
>
<rect
x="4"
y="4"
width="16"
height="16"
rx="2"
stroke="currentColor"
stroke-width="1.5"
/>
<line
x1="4"
y1="12"
x2="20"
y2="12"
stroke="currentColor"
stroke-width="1"
/>
<line
x1="12"
y1="4"
x2="12"
y2="20"
stroke="currentColor"
stroke-width="1"
/>
</svg>
<span>Resources</span>
</div>
<div class="tree-view">
<div class="tree-node">
<span class="tree-branch"></span>
<span class="tree-label">Loading...</span>
</div>
</div>
</div>
<!-- Services Panel -->
<div
class="monitor-panel services-panel"
hx-get="/api/monitoring/services"
hx-trigger="load, every 30s"
hx-swap="innerHTML"
>
<div class="panel-header">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
class="panel-icon"
>
<rect
x="2"
y="3"
width="20"
height="4"
rx="1"
stroke="currentColor"
stroke-width="1.5"
/>
<rect
x="2"
y="10"
width="20"
height="4"
rx="1"
stroke="currentColor"
stroke-width="1.5"
/>
<rect
x="2"
y="17"
width="20"
height="4"
rx="1"
stroke="currentColor"
stroke-width="1.5"
/>
</svg>
<span>Services</span>
</div>
<div class="tree-view">
<div class="tree-node">
<span class="tree-branch"></span>
<span class="tree-label">Loading...</span>
</div>
</div>
</div>
<!-- Bots Panel -->
<div
class="monitor-panel bots-panel"
hx-get="/api/monitoring/bots"
hx-trigger="load, every 30s"
hx-swap="innerHTML"
>
<div class="panel-header">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
class="panel-icon"
>
<rect
x="4"
y="4"
width="16"
height="14"
rx="3"
stroke="currentColor"
stroke-width="1.5"
/>
<circle cx="9" cy="10" r="1.5" fill="currentColor" />
<circle cx="15" cy="10" r="1.5" fill="currentColor" />
<path
d="M9 14 Q12 16 15 14"
stroke="currentColor"
stroke-width="1.5"
fill="none"
/>
</svg>
<span>Active Bots</span>
</div>
<div class="tree-view">
<div class="tree-node">
<span class="tree-branch"></span>
<span class="tree-label">Loading...</span>
</div>
</div>
</div>
</div>
<!-- Hidden service status updater -->
<div
id="service-status-container"
style="display: none"
hx-get="/api/monitoring/services/status"
hx-trigger="load, every 30s"
hx-swap="innerHTML"
></div>
<style>
.monitoring-container {
padding: 1.5rem;
max-width: 100%;
margin: 0 auto;
background: var(--bg-dark, #0f172a);
min-height: 100vh;
}
.monitoring-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-color, #334155);
}
.monitoring-header h2 {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 1.5rem;
font-weight: 600;
color: var(--text-primary, #f8fafc);
margin: 0;
}
.header-icon {
color: var(--primary-color, #3b82f6);
}
.header-actions {
display: flex;
align-items: center;
gap: 1rem;
}
.view-toggle {
background: var(--bg-surface, #1e293b);
border: 1px solid var(--border-color, #334155);
border-radius: 8px;
padding: 0.5rem;
cursor: pointer;
color: var(--text-secondary, #94a3b8);
transition: all 0.2s;
}
.view-toggle:hover {
border-color: var(--primary-color, #3b82f6);
color: var(--primary-color, #3b82f6);
}
.last-updated {
font-size: 0.875rem;
color: var(--text-secondary, #94a3b8);
}
/* Live Visualization */
.live-visualization {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow-x: auto;
}
.live-svg {
width: 100%;
height: auto;
max-height: calc(100vh - 120px);
min-width: 900px;
}
/* Animations */
@keyframes rotate-slow {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes pulse-ring {
0%,
100% {
r: 63;
opacity: 0.6;
}
50% {
r: 67;
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: 4;
}
50% {
r: 6;
}
}
@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);
}
/* Service nodes */
.service-node {
cursor: pointer;
transition: transform 0.2s ease;
}
.service-node:hover {
transform: scale(1.05);
}
/* Resource bars */
.resource-fill {
transition: width 0.5s ease-out;
}
/* Grid View Styles */
.monitoring-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1.5rem;
}
.monitor-panel {
background: var(--card-bg, #1e293b);
border: 1px solid var(--border-color, #334155);
border-radius: 0.75rem;
padding: 1.25rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.panel-header {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
font-size: 1rem;
color: var(--text-primary, #f8fafc);
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--border-color, #334155);
}
.panel-icon {
color: var(--primary-color, #3b82f6);
}
.tree-view {
font-family:
system-ui,
-apple-system,
sans-serif;
font-size: 0.875rem;
}
.tree-node {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0;
}
.tree-branch {
width: 1.25rem;
height: 1.25rem;
position: relative;
flex-shrink: 0;
}
.tree-branch::before {
content: "";
position: absolute;
left: 0.5rem;
top: 0;
width: 1px;
height: 100%;
background: var(--primary-color, #3b82f6);
opacity: 0.4;
}
.tree-branch::after {
content: "";
position: absolute;
left: 0.5rem;
top: 50%;
width: 0.5rem;
height: 1px;
background: var(--primary-color, #3b82f6);
opacity: 0.6;
}
.tree-label {
color: var(--text-secondary, #94a3b8);
flex: 1;
}
.tree-value {
font-weight: 600;
color: var(--text-primary, #f8fafc);
}
@media (max-width: 768px) {
.monitoring-grid {
grid-template-columns: 1fr;
}
.monitoring-header {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.live-svg {
min-width: 600px;
}
}
</style>
<script>
// View toggle functionality
document
.getElementById("view-toggle")
.addEventListener("click", function () {
const liveView = document.getElementById("live-view");
const gridView = document.getElementById("grid-view");
if (liveView.style.display === "none") {
liveView.style.display = "flex";
gridView.style.display = "none";
} else {
liveView.style.display = "none";
gridView.style.display = "grid";
}
});
// Update service status dots
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) {
// Silent fail for non-JSON responses
}
}
});
// Keyboard shortcut: R to refresh, V to toggle view
document.addEventListener("keydown", function (e) {
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA")
return;
if (e.key === "r" && !e.ctrlKey && !e.metaKey) {
htmx.trigger(document.body, "htmx:load");
}
if (e.key === "v" && !e.ctrlKey && !e.metaKey) {
document.getElementById("view-toggle").click();
}
});
</script>
</div>