diff --git a/apps/stock/web-api/src/routes/monitoring.routes.ts b/apps/stock/web-api/src/routes/monitoring.routes.ts index 8efd6e7..e99adf3 100644 --- a/apps/stock/web-api/src/routes/monitoring.routes.ts +++ b/apps/stock/web-api/src/routes/monitoring.routes.ts @@ -179,5 +179,82 @@ export function createMonitoringRoutes(container: IServiceContainer) { }); }); + /** + * Get service status for all microservices + */ + monitoring.get('/services', async (c) => { + try { + const services = await monitoringService.getServiceStatus(); + return c.json({ services }); + } catch (error) { + return c.json({ + error: 'Failed to retrieve service status', + message: error instanceof Error ? error.message : 'Unknown error', + }, 500); + } + }); + + /** + * Get proxy statistics + */ + monitoring.get('/proxies', async (c) => { + try { + const stats = await monitoringService.getProxyStats(); + return c.json(stats || { enabled: false }); + } catch (error) { + return c.json({ + error: 'Failed to retrieve proxy statistics', + message: error instanceof Error ? error.message : 'Unknown error', + }, 500); + } + }); + + /** + * Get comprehensive system overview + */ + monitoring.get('/overview', async (c) => { + try { + const overview = await monitoringService.getSystemOverview(); + return c.json(overview); + } catch (error) { + return c.json({ + error: 'Failed to retrieve system overview', + message: error instanceof Error ? error.message : 'Unknown error', + }, 500); + } + }); + + /** + * Test direct BullMQ queue access + */ + monitoring.get('/test/queue/:name', async (c) => { + const queueName = c.req.param('name'); + const { Queue } = await import('bullmq'); + + const connection = { + host: 'localhost', + port: 6379, + db: 1, + }; + + const queue = new Queue(`{${queueName}}`, { connection }); + + try { + const counts = await queue.getJobCounts(); + await queue.close(); + return c.json({ + queueName, + bullmqName: `{${queueName}}`, + counts + }); + } catch (error: any) { + await queue.close(); + return c.json({ + queueName, + error: error.message + }, 500); + } + }); + return monitoring; } \ No newline at end of file diff --git a/apps/stock/web-api/src/services/monitoring.service.ts b/apps/stock/web-api/src/services/monitoring.service.ts index 40a2268..289ee64 100644 --- a/apps/stock/web-api/src/services/monitoring.service.ts +++ b/apps/stock/web-api/src/services/monitoring.service.ts @@ -11,8 +11,12 @@ import type { DatabaseStats, SystemHealth, ServiceMetrics, - MetricSnapshot + MetricSnapshot, + ServiceStatus, + ProxyStats, + SystemOverview } from '../types/monitoring.types'; +import * as os from 'os'; export class MonitoringService { private readonly logger = getLogger('monitoring-service'); @@ -32,36 +36,30 @@ export class MonitoringService { }; } - // Get Redis/Dragonfly info - const info = await this.container.cache.info(); - const dbSize = await this.container.cache.dbsize(); - - // Parse memory stats from info - const memoryUsed = this.parseInfoValue(info, 'used_memory'); - const memoryPeak = this.parseInfoValue(info, 'used_memory_peak'); - - // Parse stats - const hits = this.parseInfoValue(info, 'keyspace_hits'); - const misses = this.parseInfoValue(info, 'keyspace_misses'); - const evictedKeys = this.parseInfoValue(info, 'evicted_keys'); - const expiredKeys = this.parseInfoValue(info, 'expired_keys'); + // Check if cache is connected using the isReady method + const isConnected = this.container.cache.isReady(); + if (!isConnected) { + return { + provider: 'dragonfly', + connected: false, + }; + } + + // Get cache stats from the provider + const cacheStats = this.container.cache.getStats(); + // Since we can't access Redis info directly, we'll use what's available return { provider: 'dragonfly', connected: true, - uptime: this.parseInfoValue(info, 'uptime_in_seconds'), - memoryUsage: { - used: memoryUsed, - peak: memoryPeak, - }, + uptime: cacheStats.uptime, stats: { - hits, - misses, - keys: dbSize, - evictedKeys, - expiredKeys, + hits: cacheStats.hits, + misses: cacheStats.misses, + keys: 0, // We can't get total keys without direct Redis access + evictedKeys: 0, + expiredKeys: 0, }, - info: this.parseRedisInfo(info), }; } catch (error) { this.logger.error('Failed to get cache stats', { error }); @@ -80,47 +78,52 @@ export class MonitoringService { try { if (!this.container.queue) { + this.logger.warn('No queue manager available'); return stats; } - // Get all queue names from the queue manager - const queueManager = this.container.queue; - const queueNames = ['default', 'proxy', 'qm', 'ib', 'ceo', 'webshare']; // Add your queue names + // Get all queue names from the SmartQueueManager + const queueManager = this.container.queue as any; + this.logger.debug('Queue manager type:', { + type: queueManager.constructor.name, + hasGetAllQueues: typeof queueManager.getAllQueues === 'function', + hasQueues: !!queueManager.queues, + hasGetQueue: typeof queueManager.getQueue === 'function' + }); + // Always use the known queue names since web-api doesn't create worker queues + const queueNames = ['proxy', 'qm', 'ib', 'ceo', 'webshare', 'exchanges', 'symbols']; + this.logger.debug('Using known queue names', { count: queueNames.length, names: queueNames }); + + // Create BullMQ queues directly with the correct format for (const queueName of queueNames) { try { - const queue = queueManager.getQueue(queueName); - if (!queue) continue; - - const [waiting, active, completed, failed, delayed, paused] = await Promise.all([ - queue.getWaitingCount(), - queue.getActiveCount(), - queue.getCompletedCount(), - queue.getFailedCount(), - queue.getDelayedCount(), - queue.getPausedCount(), - ]); - - // Get worker info if available - const workers = queueManager.getWorker(queueName); - const workerInfo = workers ? { - count: 1, // Assuming single worker per queue - concurrency: workers.concurrency || 1, - } : undefined; - + // Import BullMQ directly to create queue instances + const { Queue: BullMQQueue } = await import('bullmq'); + const connection = { + host: 'localhost', + port: 6379, + db: 1, // Queue DB + }; + + // Create BullMQ queue with the correct format + const bullQueue = new BullMQQueue(`{${queueName}}`, { connection }); + + // Get stats directly from BullMQ + const queueStats = await this.getQueueStatsForBullQueue(bullQueue, queueName); + stats.push({ name: queueName, connected: true, - jobs: { - waiting, - active, - completed, - failed, - delayed, - paused, + jobs: queueStats, + workers: { + count: 0, + concurrency: 1, }, - workers: workerInfo, }); + + // Close the queue connection after getting stats + await bullQueue.close(); } catch (error) { this.logger.warn(`Failed to get stats for queue ${queueName}`, { error }); stats.push({ @@ -144,6 +147,162 @@ export class MonitoringService { return stats; } + /** + * Get stats for a BullMQ queue directly + */ + private async getQueueStatsForBullQueue(bullQueue: any, queueName: string) { + try { + // BullMQ provides getJobCounts which returns all counts at once + const counts = await bullQueue.getJobCounts(); + + return { + waiting: counts.waiting || 0, + active: counts.active || 0, + completed: counts.completed || 0, + failed: counts.failed || 0, + delayed: counts.delayed || 0, + paused: counts.paused || 0, + prioritized: counts.prioritized || 0, + 'waiting-children': counts['waiting-children'] || 0, + }; + } catch (error) { + this.logger.error(`Failed to get stats for BullMQ queue ${queueName}`, { error }); + // Fallback to individual methods + try { + const [waiting, active, completed, failed, delayed, paused] = await Promise.all([ + bullQueue.getWaitingCount(), + bullQueue.getActiveCount(), + bullQueue.getCompletedCount(), + bullQueue.getFailedCount(), + bullQueue.getDelayedCount(), + bullQueue.getPausedCount ? bullQueue.getPausedCount() : 0 + ]); + + return { + waiting, + active, + completed, + failed, + delayed, + paused, + }; + } catch (fallbackError) { + this.logger.error(`Fallback also failed for queue ${queueName}`, { fallbackError }); + return this.getQueueStatsForQueue(bullQueue, queueName); + } + } + } + + /** + * Get stats for a specific queue + */ + private async getQueueStatsForQueue(queue: any, queueName: string) { + // Check if it has the getStats method + if (queue.getStats && typeof queue.getStats === 'function') { + const stats = await queue.getStats(); + return { + waiting: stats.waiting || 0, + active: stats.active || 0, + completed: stats.completed || 0, + failed: stats.failed || 0, + delayed: stats.delayed || 0, + paused: stats.paused || 0, + }; + } + + // Try individual count methods + const [waiting, active, completed, failed, delayed] = await Promise.all([ + this.safeGetCount(queue, 'getWaitingCount', 'getWaiting'), + this.safeGetCount(queue, 'getActiveCount', 'getActive'), + this.safeGetCount(queue, 'getCompletedCount', 'getCompleted'), + this.safeGetCount(queue, 'getFailedCount', 'getFailed'), + this.safeGetCount(queue, 'getDelayedCount', 'getDelayed'), + ]); + + const paused = await this.safeGetPausedStatus(queue); + + return { + waiting, + active, + completed, + failed, + delayed, + paused, + }; + } + + /** + * Safely get count from queue + */ + private async safeGetCount(queue: any, ...methodNames: string[]): Promise { + for (const methodName of methodNames) { + if (queue[methodName] && typeof queue[methodName] === 'function') { + try { + const result = await queue[methodName](); + return Array.isArray(result) ? result.length : (result || 0); + } catch (e) { + // Continue to next method + } + } + } + return 0; + } + + /** + * Get paused status + */ + private async safeGetPausedStatus(queue: any): Promise { + try { + if (queue.isPaused && typeof queue.isPaused === 'function') { + const isPaused = await queue.isPaused(); + return isPaused ? 1 : 0; + } + if (queue.getPausedCount && typeof queue.getPausedCount === 'function') { + return await queue.getPausedCount(); + } + } catch (e) { + // Ignore + } + return 0; + } + + /** + * Get worker info for a queue + */ + private getWorkerInfo(queue: any, queueManager: any, queueName: string) { + try { + // Check queue itself for worker info + if (queue.workers && Array.isArray(queue.workers)) { + return { + count: queue.workers.length, + concurrency: queue.workers[0]?.concurrency || 1, + }; + } + + // Check queue manager for worker config + if (queueManager.config?.defaultQueueOptions) { + const options = queueManager.config.defaultQueueOptions; + return { + count: options.workers || 1, + concurrency: options.concurrency || 1, + }; + } + + // Check for getWorkerCount method + if (queue.getWorkerCount && typeof queue.getWorkerCount === 'function') { + const count = queue.getWorkerCount(); + return { + count, + concurrency: 1, + }; + } + } catch (e) { + // Ignore + } + + return undefined; + } + /** * Get database statistics */ @@ -187,7 +346,8 @@ export class MonitoringService { if (this.container.mongodb) { try { const startTime = Date.now(); - const db = this.container.mongodb.db(); + const mongoClient = this.container.mongodb as any; // This is MongoDBClient + const db = mongoClient.getDatabase(); await db.admin().ping(); const latency = Date.now() - startTime; @@ -252,8 +412,10 @@ export class MonitoringService { this.getDatabaseStats(), ]); - const memory = process.memoryUsage(); + const processMemory = process.memoryUsage(); + const systemMemory = this.getSystemMemory(); const uptime = Date.now() - this.startTime; + const cpuInfo = this.getCpuUsage(); // Determine overall health status const errors: string[] = []; @@ -280,10 +442,15 @@ export class MonitoringService { timestamp: new Date().toISOString(), uptime, memory: { - used: memory.heapUsed, - total: memory.heapTotal, - percentage: (memory.heapUsed / memory.heapTotal) * 100, + used: systemMemory.used, + total: systemMemory.total, + percentage: systemMemory.percentage, + heap: { + used: processMemory.heapUsed, + total: processMemory.heapTotal, + }, }, + cpu: cpuInfo, services: { cache: cacheStats, queues: queueStats, @@ -353,4 +520,217 @@ export class MonitoringService { return result; } + + /** + * Get service status for all microservices + */ + async getServiceStatus(): Promise { + const services: ServiceStatus[] = []; + + // Define service endpoints + const serviceEndpoints = [ + { name: 'data-ingestion', port: 2001, path: '/health' }, + { name: 'data-pipeline', port: 2002, path: '/health' }, + { name: 'web-api', port: 2003, path: '/health' }, + ]; + + for (const service of serviceEndpoints) { + try { + const startTime = Date.now(); + const response = await fetch(`http://localhost:${service.port}${service.path}`, { + signal: AbortSignal.timeout(5000), // 5 second timeout + }); + const latency = Date.now() - startTime; + + if (response.ok) { + const data = await response.json(); + services.push({ + name: service.name, + version: data.version || '1.0.0', + status: 'running', + port: service.port, + uptime: data.uptime || 0, + lastCheck: new Date().toISOString(), + healthy: true, + }); + } else { + services.push({ + name: service.name, + version: 'unknown', + status: 'error', + port: service.port, + uptime: 0, + lastCheck: new Date().toISOString(), + healthy: false, + error: `HTTP ${response.status}`, + }); + } + } catch (error) { + services.push({ + name: service.name, + version: 'unknown', + status: 'stopped', + port: service.port, + uptime: 0, + lastCheck: new Date().toISOString(), + healthy: false, + error: error instanceof Error ? error.message : 'Connection failed', + }); + } + } + + // Add current service (web-api) + services.push({ + name: 'web-api', + version: '1.0.0', + status: 'running', + port: process.env.PORT ? parseInt(process.env.PORT) : 2003, + uptime: Date.now() - this.startTime, + lastCheck: new Date().toISOString(), + healthy: true, + }); + + return services; + } + + /** + * Get proxy statistics + */ + async getProxyStats(): Promise { + try { + if (!this.container.proxy) { + return { + enabled: false, + totalProxies: 0, + workingProxies: 0, + failedProxies: 0, + }; + } + + const proxyManager = this.container.proxy as any; + + // Check if proxy manager is ready + if (!proxyManager.isReady || !proxyManager.isReady()) { + return { + enabled: true, + totalProxies: 0, + workingProxies: 0, + failedProxies: 0, + }; + } + + const stats = proxyManager.getStats ? proxyManager.getStats() : null; + const lastFetchTime = proxyManager.getLastFetchTime ? proxyManager.getLastFetchTime() : null; + + return { + enabled: true, + totalProxies: stats?.total || 0, + workingProxies: stats?.working || 0, + failedProxies: stats?.failed || 0, + lastUpdate: stats?.lastUpdate ? new Date(stats.lastUpdate).toISOString() : undefined, + lastFetchTime: lastFetchTime ? new Date(lastFetchTime).toISOString() : undefined, + }; + } catch (error) { + this.logger.error('Failed to get proxy stats', { error }); + return null; + } + } + + /** + * Get comprehensive system overview + */ + async getSystemOverview(): Promise { + const [services, health, proxies] = await Promise.all([ + this.getServiceStatus(), + this.getSystemHealth(), + this.getProxyStats(), + ]); + + return { + services, + health, + proxies: proxies || undefined, + environment: { + nodeVersion: process.version, + platform: os.platform(), + architecture: os.arch(), + hostname: os.hostname(), + }, + }; + } + + /** + * Get detailed CPU usage + */ + private getCpuUsage() { + const cpus = os.cpus(); + let totalIdle = 0; + let totalTick = 0; + + cpus.forEach(cpu => { + for (const type in cpu.times) { + totalTick += cpu.times[type as keyof typeof cpu.times]; + } + totalIdle += cpu.times.idle; + }); + + const idle = totalIdle / cpus.length; + const total = totalTick / cpus.length; + const usage = 100 - ~~(100 * idle / total); + + return { + usage, + loadAverage: os.loadavg(), + cores: cpus.length, + }; + } + + /** + * Get system memory info + */ + private getSystemMemory() { + const totalMem = os.totalmem(); + const freeMem = os.freemem(); + + // On Linux, freeMem includes buffers/cache, but we want "available" memory + // which better represents memory that can be used by applications + let availableMem = freeMem; + + // Try to read from /proc/meminfo for more accurate memory stats on Linux + if (os.platform() === 'linux') { + try { + const fs = require('fs'); + const meminfo = fs.readFileSync('/proc/meminfo', 'utf8'); + const lines = meminfo.split('\n'); + + let memAvailable = 0; + let memTotal = 0; + + for (const line of lines) { + if (line.startsWith('MemAvailable:')) { + memAvailable = parseInt(line.split(/\s+/)[1], 10) * 1024; // Convert from KB to bytes + } else if (line.startsWith('MemTotal:')) { + memTotal = parseInt(line.split(/\s+/)[1], 10) * 1024; + } + } + + if (memAvailable > 0) { + availableMem = memAvailable; + } + } catch (error) { + // Fallback to os.freemem() if we can't read /proc/meminfo + this.logger.debug('Could not read /proc/meminfo', { error }); + } + } + + const usedMem = totalMem - availableMem; + + return { + total: totalMem, + used: usedMem, + free: freeMem, + available: availableMem, + percentage: (usedMem / totalMem) * 100, + }; + } } \ No newline at end of file diff --git a/apps/stock/web-api/src/types/monitoring.types.ts b/apps/stock/web-api/src/types/monitoring.types.ts index f9c0992..8d41532 100644 --- a/apps/stock/web-api/src/types/monitoring.types.ts +++ b/apps/stock/web-api/src/types/monitoring.types.ts @@ -31,6 +31,8 @@ export interface QueueStats { failed: number; delayed: number; paused: number; + prioritized?: number; + 'waiting-children'?: number; }; workers?: { count: number; @@ -90,4 +92,36 @@ export interface ServiceMetrics { averageResponseTime: MetricSnapshot; errorRate: MetricSnapshot; activeConnections: MetricSnapshot; +} + +export interface ServiceStatus { + name: string; + version: string; + status: 'running' | 'stopped' | 'error'; + port?: number; + uptime: number; + lastCheck: string; + healthy: boolean; + error?: string; +} + +export interface ProxyStats { + enabled: boolean; + totalProxies: number; + workingProxies: number; + failedProxies: number; + lastUpdate?: string; + lastFetchTime?: string; +} + +export interface SystemOverview { + services: ServiceStatus[]; + health: SystemHealth; + proxies?: ProxyStats; + environment: { + nodeVersion: string; + platform: string; + architecture: string; + hostname: string; + }; } \ No newline at end of file diff --git a/apps/stock/web-app/src/components/layout/Layout.tsx b/apps/stock/web-app/src/components/layout/Layout.tsx index dc777ca..5842c50 100644 --- a/apps/stock/web-app/src/components/layout/Layout.tsx +++ b/apps/stock/web-app/src/components/layout/Layout.tsx @@ -29,8 +29,8 @@ export function Layout() {
-
-
+
+
diff --git a/apps/stock/web-app/src/components/ui/button.tsx b/apps/stock/web-app/src/components/ui/Button.tsx similarity index 100% rename from apps/stock/web-app/src/components/ui/button.tsx rename to apps/stock/web-app/src/components/ui/Button.tsx diff --git a/apps/stock/web-app/src/components/ui/Card.tsx b/apps/stock/web-app/src/components/ui/Card.tsx index 57dadda..599a6ed 100644 --- a/apps/stock/web-app/src/components/ui/Card.tsx +++ b/apps/stock/web-app/src/components/ui/Card.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from 'react'; +import type { ReactNode } from 'react'; import { cn } from '@/lib/utils'; interface CardProps { @@ -11,7 +11,7 @@ export function Card({ children, className, hover = false }: CardProps) { return (
+
{icon}
diff --git a/apps/stock/web-app/src/components/ui/index.ts b/apps/stock/web-app/src/components/ui/index.ts index 30766a7..e065fee 100644 --- a/apps/stock/web-app/src/components/ui/index.ts +++ b/apps/stock/web-app/src/components/ui/index.ts @@ -1,5 +1,6 @@ -export { Card, CardHeader, CardContent } from './Card'; -export { StatCard } from './StatCard'; +export { Button } from './Button'; +export { Card, CardContent, CardHeader } from './Card'; export { DataTable } from './DataTable'; -export { Dialog, DialogContent, DialogHeader, DialogTitle } from './dialog'; -export { Button } from './button'; +export { Dialog, DialogContent, DialogHeader, DialogTitle } from './Dialog'; +export { StatCard } from './StatCard'; + diff --git a/apps/stock/web-app/src/features/monitoring/MonitoringPage.tsx b/apps/stock/web-app/src/features/monitoring/MonitoringPage.tsx index 35e4a2c..043077e 100644 --- a/apps/stock/web-app/src/features/monitoring/MonitoringPage.tsx +++ b/apps/stock/web-app/src/features/monitoring/MonitoringPage.tsx @@ -3,49 +3,105 @@ */ import React, { useState } from 'react'; -import { useSystemHealth, useCacheStats, useQueueStats, useDatabaseStats } from './hooks'; -import { SystemHealthCard, CacheStatsCard, QueueStatsTable, DatabaseStatsGrid } from './components'; +import { + CacheStatsCard, + DatabaseStatsGrid, + ProxyStatsCard, + QueueStatsTable, + ServiceStatusGrid, + SystemHealthCard +} from './components'; +import { + useCacheStats, + useDatabaseStats, + useProxyStats, + useQueueStats, + useServiceStatus, + useSystemHealth, + useSystemOverview +} from './hooks'; export function MonitoringPage() { const [refreshInterval, setRefreshInterval] = useState(5000); // 5 seconds default + const [useOverview, setUseOverview] = useState(false); // Toggle between individual calls and overview - const { data: health, loading: healthLoading, error: healthError } = useSystemHealth(refreshInterval); - const { data: cache, loading: cacheLoading, error: cacheError } = useCacheStats(refreshInterval); - const { data: queues, loading: queuesLoading, error: queuesError } = useQueueStats(refreshInterval); - const { data: databases, loading: dbLoading, error: dbError } = useDatabaseStats(refreshInterval); + // Individual API calls + const { data: health, loading: healthLoading, error: healthError } = useSystemHealth(useOverview ? 0 : refreshInterval); + const { data: cache, loading: cacheLoading, error: cacheError } = useCacheStats(useOverview ? 0 : refreshInterval); + const { data: queues, loading: queuesLoading, error: queuesError } = useQueueStats(useOverview ? 0 : refreshInterval); + const { data: databases, loading: dbLoading, error: dbError } = useDatabaseStats(useOverview ? 0 : refreshInterval); + const { data: services, loading: servicesLoading, error: servicesError } = useServiceStatus(useOverview ? 0 : refreshInterval); + const { data: proxies, loading: proxiesLoading, error: proxiesError } = useProxyStats(useOverview ? 0 : refreshInterval); + + // Combined overview call + const { data: overview, loading: overviewLoading, error: overviewError } = useSystemOverview(useOverview ? refreshInterval : 0); const handleRefreshIntervalChange = (e: React.ChangeEvent) => { setRefreshInterval(Number(e.target.value)); }; - if (healthLoading || cacheLoading || queuesLoading || dbLoading) { + const handleDataSourceToggle = () => { + setUseOverview(!useOverview); + }; + + // Use overview data if enabled + const displayHealth = useOverview && overview ? overview.health : health; + const displayServices = useOverview && overview ? overview.services : services; + const displayProxies = useOverview && overview ? overview.proxies : proxies; + const displayCache = useOverview && overview ? overview.health.services.cache : cache; + const displayQueues = useOverview && overview ? overview.health.services.queues : queues; + const displayDatabases = useOverview && overview ? overview.health.services.databases : databases; + + const isLoading = useOverview + ? overviewLoading + : (healthLoading || cacheLoading || queuesLoading || dbLoading || servicesLoading || proxiesLoading); + + const errors = useOverview + ? (overviewError ? [overviewError] : []) + : [healthError, cacheError, queuesError, dbError, servicesError, proxiesError].filter(Boolean); + + if (isLoading) { return (
-
-

Loading monitoring data...

+
+

Loading monitoring data...

); } - const hasErrors = healthError || cacheError || queuesError || dbError; - return ( -
-
-
-

System Monitoring

+
+
+
+

System Monitoring

+
-
+
+
-
- {hasErrors && ( -
-

Errors occurred while fetching data:

-
    - {healthError &&
  • System Health: {healthError}
  • } - {cacheError &&
  • Cache Stats: {cacheError}
  • } - {queuesError &&
  • Queue Stats: {queuesError}
  • } - {dbError &&
  • Database Stats: {dbError}
  • } + {errors.length > 0 && ( +
    +

    Errors occurred while fetching data:

    +
      + {errors.map((error, index) => ( +
    • {error}
    • + ))}
    )} -
    - {/* System Health */} - {health && ( -
    -
    - + {useOverview && overview && ( +
    +

    System Environment

    +
    +
    + Node: {overview.environment.nodeVersion}
    -
    - {cache && } +
    + Platform: {overview.environment.platform} +
    +
    + Architecture: {overview.environment.architecture} +
    +
    + Hostname: {overview.environment.hostname} +
    +
    +
    + )} + +
    + {/* Service Status */} + {displayServices && displayServices.length > 0 && ( +
    +

    Microservices Status

    + +
    + )} + + {/* System Health and Cache */} + {displayHealth && ( +
    +
    + +
    +
    + {displayCache && } +
    +
    + {displayProxies && }
    )} {/* Database Stats */} - {databases && databases.length > 0 && ( + {displayDatabases && displayDatabases.length > 0 && (
    -

    Database Connections

    - +

    Database Connections

    +
    )} {/* Queue Stats */} - {queues && queues.length > 0 && ( + {displayQueues && displayQueues.length > 0 && (
    -

    Queue Status

    - +

    Queue Status

    +
    )}
    diff --git a/apps/stock/web-app/src/features/monitoring/README.md b/apps/stock/web-app/src/features/monitoring/README.md new file mode 100644 index 0000000..33bab17 --- /dev/null +++ b/apps/stock/web-app/src/features/monitoring/README.md @@ -0,0 +1,39 @@ +# Monitoring Components + +This directory contains monitoring components that have been refactored to use standardized UI components with a consistent dark theme. + +## Standardized Components + +### StatusBadge +Used for displaying status indicators with consistent styling: +- `ConnectionStatus` - Shows connected/disconnected state +- `HealthStatus` - Shows healthy/unhealthy state +- `ServiceStatusIndicator` - Shows service status as a colored dot + +### MetricCard +Displays metrics with optional progress bars in a consistent card layout. + +### Cards +- `ServiceCard` - Displays individual service status +- `DatabaseCard` - Displays database connection info +- `SystemHealthCard` - Shows system health overview +- `CacheStatsCard` - Shows cache statistics +- `ProxyStatsCard` - Shows proxy status +- `QueueStatsTable` - Displays queue statistics in a table + +## Theme Colors + +All components now use the standardized color palette from the Tailwind config: +- Background: `bg-surface-secondary` (dark surfaces) +- Borders: `border-border` +- Text: `text-text-primary`, `text-text-secondary`, `text-text-muted` +- Status colors: `text-success`, `text-danger`, `text-warning` +- Primary accent: `text-primary-400`, `bg-primary-500/10` + +## Utilities + +Common formatting functions are available in `utils/formatters.ts`: +- `formatUptime()` - Formats milliseconds to human-readable uptime +- `formatBytes()` - Formats bytes to KB/MB/GB +- `formatNumber()` - Adds thousand separators +- `formatPercentage()` - Formats numbers as percentages \ No newline at end of file diff --git a/apps/stock/web-app/src/features/monitoring/SPACING.md b/apps/stock/web-app/src/features/monitoring/SPACING.md new file mode 100644 index 0000000..0359772 --- /dev/null +++ b/apps/stock/web-app/src/features/monitoring/SPACING.md @@ -0,0 +1,44 @@ +# Monitoring Components - Spacing Guidelines + +This document outlines the standardized spacing used across monitoring components to maximize screen real estate. + +## Spacing Standards + +### Page Layout +- Main container: `flex flex-col h-full space-y-4` (16px vertical gaps) +- No outer padding - handled by parent layout +- Section spacing: `space-y-4` between major sections + +### Cards +- Card padding: `p-4` (16px) for main cards, `p-3` (12px) for compact cards +- Card header: `pb-3` (12px bottom padding) +- Card content spacing: `space-y-3` (12px gaps) +- Grid gaps: `gap-3` (12px) or `gap-4` (16px) + +### Typography +- Page title: `text-lg font-bold mb-2` +- Section headings: `text-lg font-semibold mb-3` +- Card titles: `text-base font-semibold` +- Large values: `text-xl` or `text-lg` +- Regular text: `text-sm` +- Small text/labels: `text-xs` + +### Specific Components + +**ServiceCard**: `p-3` with `space-y-1.5` and `text-xs` +**DatabaseCard**: `p-4` with `space-y-2` +**SystemHealthCard**: `p-4` with `space-y-3` +**CacheStatsCard**: `p-4` with `space-y-3` +**ProxyStatsCard**: `p-4` with `space-y-3` +**QueueStatsTable**: `p-4` with `text-xs` table + +### Grids +- Service grid: `gap-3` +- Database grid: `gap-3` +- Main layout grid: `gap-4` + +## Benefits +- Maximizes usable screen space +- Consistent with dashboard/exchanges pages +- More data visible without scrolling +- Clean, compact appearance \ No newline at end of file diff --git a/apps/stock/web-app/src/features/monitoring/components/CacheStatsCard.tsx b/apps/stock/web-app/src/features/monitoring/components/CacheStatsCard.tsx index 39bfe06..08a6489 100644 --- a/apps/stock/web-app/src/features/monitoring/components/CacheStatsCard.tsx +++ b/apps/stock/web-app/src/features/monitoring/components/CacheStatsCard.tsx @@ -2,95 +2,91 @@ * Cache Statistics Card Component */ -import React from 'react'; -import { Card } from '../../../components/ui/Card'; import type { CacheStats } from '../types'; +import { Card, CardHeader, CardContent } from '@/components/ui/Card'; +import { ConnectionStatus } from './StatusBadge'; +import { formatBytes, formatNumber, formatPercentage } from '../utils/formatters'; interface CacheStatsCardProps { stats: CacheStats; } export function CacheStatsCard({ stats }: CacheStatsCardProps) { - const formatBytes = (bytes: number) => { - const mb = bytes / 1024 / 1024; - return mb.toFixed(2) + ' MB'; - }; - const hitRate = stats.stats && (stats.stats.hits + stats.stats.misses) > 0 - ? (stats.stats.hits / (stats.stats.hits + stats.stats.misses) * 100).toFixed(1) - : '0'; + ? (stats.stats.hits / (stats.stats.hits + stats.stats.misses) * 100) + : 0; return ( - -
    -

    Cache (Dragonfly)

    - - {stats.connected ? 'Connected' : 'Disconnected'} - -
    - - {stats.connected ? ( -
    - {stats.memoryUsage && ( -
    -
    -
    Memory Used
    -
    {formatBytes(stats.memoryUsage.used)}
    -
    -
    -
    Peak Memory
    -
    {formatBytes(stats.memoryUsage.peak)}
    -
    -
    - )} - - {stats.stats && ( - <> -
    -
    -
    Hit Rate
    -
    {hitRate}%
    -
    -
    -
    Total Keys
    -
    {stats.stats.keys.toLocaleString()}
    -
    -
    - -
    -
    - Hits: {stats.stats.hits.toLocaleString()} -
    -
    - Misses: {stats.stats.misses.toLocaleString()} -
    - {stats.stats.evictedKeys !== undefined && ( -
    - Evicted: {stats.stats.evictedKeys.toLocaleString()} -
    - )} - {stats.stats.expiredKeys !== undefined && ( -
    - Expired: {stats.stats.expiredKeys.toLocaleString()} -
    - )} -
    - - )} - - {stats.uptime && ( -
    - Uptime: {Math.floor(stats.uptime / 3600)}h {Math.floor((stats.uptime % 3600) / 60)}m -
    - )} + + +
    +

    Cache (Dragonfly)

    +
    - ) : ( -
    - Cache service is not available -
    - )} +
    + + + {stats.connected ? ( +
    + {stats.memoryUsage && ( +
    +
    +
    Memory Used
    +
    {formatBytes(stats.memoryUsage.used)}
    +
    +
    +
    Peak Memory
    +
    {formatBytes(stats.memoryUsage.peak)}
    +
    +
    + )} + + {stats.stats && ( + <> +
    +
    +
    Hit Rate
    +
    {formatPercentage(hitRate)}
    +
    +
    +
    Total Keys
    +
    {formatNumber(stats.stats.keys)}
    +
    +
    + +
    +
    + Hits: {formatNumber(stats.stats.hits)} +
    +
    + Misses: {formatNumber(stats.stats.misses)} +
    + {stats.stats.evictedKeys !== undefined && ( +
    + Evicted: {formatNumber(stats.stats.evictedKeys)} +
    + )} + {stats.stats.expiredKeys !== undefined && ( +
    + Expired: {formatNumber(stats.stats.expiredKeys)} +
    + )} +
    + + )} + + {stats.uptime && ( +
    + Uptime: {Math.floor(stats.uptime / 3600)}h {Math.floor((stats.uptime % 3600) / 60)}m +
    + )} +
    + ) : ( +
    + Cache service is not available +
    + )} +
    ); } \ No newline at end of file diff --git a/apps/stock/web-app/src/features/monitoring/components/DatabaseCard.tsx b/apps/stock/web-app/src/features/monitoring/components/DatabaseCard.tsx new file mode 100644 index 0000000..aa0bd77 --- /dev/null +++ b/apps/stock/web-app/src/features/monitoring/components/DatabaseCard.tsx @@ -0,0 +1,106 @@ +import { Card, CardHeader, CardContent } from '@/components/ui/Card'; +import { ConnectionStatus } from './StatusBadge'; +import { formatPercentage } from '../utils/formatters'; +import type { DatabaseStats } from '../types'; + +interface DatabaseCardProps { + database: DatabaseStats; +} + +export function DatabaseCard({ database }: DatabaseCardProps) { + const getDbIcon = (type: string) => { + switch (type) { + case 'postgres': + return '🐘'; + case 'mongodb': + return '🍃'; + case 'questdb': + return '⚡'; + default: + return '💾'; + } + }; + + return ( + + +
    +
    + {getDbIcon(database.type)} +

    {database.name}

    +
    + +
    +
    + + + {database.connected ? ( +
    + {database.latency !== undefined && ( +
    +
    Latency
    +
    {database.latency}ms
    +
    + )} + + {database.pool && ( +
    +
    Connection Pool
    +
    +
    + Active:{' '} + {database.pool.active} +
    +
    + Idle:{' '} + {database.pool.idle} +
    +
    + Size:{' '} + {database.pool.size} +
    +
    + Max:{' '} + {database.pool.max} +
    +
    + + {database.pool.max > 0 && ( +
    +
    +
    +
    +
    + {formatPercentage((database.pool.size / database.pool.max) * 100, 0)} utilized +
    +
    + )} +
    + )} + + {database.type === 'mongodb' && database.stats && ( +
    +
    Version: {database.stats.version}
    + {database.stats.connections && ( +
    + Connections:{' '} + + {database.stats.connections.current}/{database.stats.connections.available} + +
    + )} +
    + )} +
    + ) : ( +
    + Database is not available +
    + )} + + + ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/monitoring/components/DatabaseStatsGrid.tsx b/apps/stock/web-app/src/features/monitoring/components/DatabaseStatsGrid.tsx index 5850057..b235b01 100644 --- a/apps/stock/web-app/src/features/monitoring/components/DatabaseStatsGrid.tsx +++ b/apps/stock/web-app/src/features/monitoring/components/DatabaseStatsGrid.tsx @@ -2,102 +2,18 @@ * Database Statistics Grid Component */ -import React from 'react'; -import { Card } from '../../../components/ui/Card'; import type { DatabaseStats } from '../types'; +import { DatabaseCard } from './DatabaseCard'; interface DatabaseStatsGridProps { databases: DatabaseStats[]; } export function DatabaseStatsGrid({ databases }: DatabaseStatsGridProps) { - const getDbIcon = (type: string) => { - switch (type) { - case 'postgres': - return '🐘'; - case 'mongodb': - return '🍃'; - case 'questdb': - return '⚡'; - default: - return '💾'; - } - }; - return ( -
    +
    {databases.map((db) => ( - -
    -
    - {getDbIcon(db.type)} -

    {db.name}

    -
    - - {db.connected ? 'Connected' : 'Disconnected'} - -
    - - {db.connected ? ( -
    - {db.latency !== undefined && ( -
    -
    Latency
    -
    {db.latency}ms
    -
    - )} - - {db.pool && ( -
    -
    Connection Pool
    -
    -
    - Active: {db.pool.active} -
    -
    - Idle: {db.pool.idle} -
    -
    - Size: {db.pool.size} -
    -
    - Max: {db.pool.max} -
    -
    - - {db.pool.max > 0 && ( -
    -
    -
    -
    -
    - {((db.pool.size / db.pool.max) * 100).toFixed(0)}% utilized -
    -
    - )} -
    - )} - - {db.type === 'mongodb' && db.stats && ( -
    -
    Version: {db.stats.version}
    - {db.stats.connections && ( -
    Connections: {db.stats.connections.current}/{db.stats.connections.available}
    - )} -
    - )} -
    - ) : ( -
    - Database is not available -
    - )} - + ))}
    ); diff --git a/apps/stock/web-app/src/features/monitoring/components/MetricCard.tsx b/apps/stock/web-app/src/features/monitoring/components/MetricCard.tsx new file mode 100644 index 0000000..d1724e9 --- /dev/null +++ b/apps/stock/web-app/src/features/monitoring/components/MetricCard.tsx @@ -0,0 +1,57 @@ +import { Card, CardHeader, CardContent } from '@/components/ui/Card'; +import { cn } from '@/lib/utils'; + +interface MetricCardProps { + title: string; + value: string | number; + subtitle?: string; + icon?: React.ReactNode; + valueClassName?: string; + progress?: { + value: number; + max: number; + label?: string; + }; +} + +export function MetricCard({ + title, + value, + subtitle, + icon, + valueClassName, + progress +}: MetricCardProps) { + return ( + + +
    +

    {title}

    + {icon &&
    {icon}
    } +
    +
    + +
    + {value} +
    + {subtitle && ( +

    {subtitle}

    + )} + {progress && ( +
    +
    + {progress.label || 'Usage'} + {((progress.value / progress.max) * 100).toFixed(0)}% +
    +
    +
    +
    +
    + )} + + + ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/monitoring/components/ProxyStatsCard.tsx b/apps/stock/web-app/src/features/monitoring/components/ProxyStatsCard.tsx new file mode 100644 index 0000000..2388b08 --- /dev/null +++ b/apps/stock/web-app/src/features/monitoring/components/ProxyStatsCard.tsx @@ -0,0 +1,90 @@ +/** + * Proxy Stats Card Component + */ + +import type { ProxyStats } from '../types'; +import { Card, CardHeader, CardContent } from '@/components/ui/Card'; +import { StatusBadge } from './StatusBadge'; +import { formatPercentage } from '../utils/formatters'; + +interface ProxyStatsCardProps { + stats: ProxyStats; +} + +export function ProxyStatsCard({ stats }: ProxyStatsCardProps) { + const successRate = stats.totalProxies > 0 + ? (stats.workingProxies / stats.totalProxies) * 100 + : 0; + + const formatDate = (dateString?: string) => { + if (!dateString) return 'Never'; + const date = new Date(dateString); + return date.toLocaleString(); + }; + + return ( + + +
    +

    Proxy Status

    + + {stats.enabled ? 'Enabled' : 'Disabled'} + +
    +
    + + + {stats.enabled ? ( +
    +
    +
    +

    Total Proxies

    +

    {stats.totalProxies}

    +
    +
    +

    Success Rate

    +

    {formatPercentage(successRate)}

    +
    +
    + +
    +
    +

    Working

    +

    {stats.workingProxies}

    +
    +
    +

    Failed

    +

    {stats.failedProxies}

    +
    +
    + +
    +
    + Last Update: + {formatDate(stats.lastUpdate)} +
    +
    + Last Fetch: + {formatDate(stats.lastFetchTime)} +
    +
    + + {stats.totalProxies === 0 && ( +
    + No proxies available. Check WebShare API configuration. +
    + )} +
    + ) : ( +
    +

    Proxy service is disabled

    +

    Enable it in the configuration to use proxies

    +
    + )} +
    +
    + ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/monitoring/components/QueueStatsTable.tsx b/apps/stock/web-app/src/features/monitoring/components/QueueStatsTable.tsx index 96d51ad..ca3b1a8 100644 --- a/apps/stock/web-app/src/features/monitoring/components/QueueStatsTable.tsx +++ b/apps/stock/web-app/src/features/monitoring/components/QueueStatsTable.tsx @@ -2,9 +2,10 @@ * Queue Statistics Table Component */ -import React from 'react'; -import { Card } from '../../../components/ui/Card'; import type { QueueStats } from '../types'; +import { Card, CardHeader, CardContent } from '@/components/ui/Card'; +import { formatNumber } from '../utils/formatters'; +import { cn } from '@/lib/utils'; interface QueueStatsTableProps { queues: QueueStats[]; @@ -13,65 +14,91 @@ interface QueueStatsTableProps { export function QueueStatsTable({ queues }: QueueStatsTableProps) { const totalJobs = (queue: QueueStats) => { const { jobs } = queue; - return jobs.waiting + jobs.active + jobs.completed + jobs.failed + jobs.delayed + jobs.paused; + return jobs.waiting + jobs.active + jobs.completed + jobs.failed + jobs.delayed + jobs.paused + + (jobs.prioritized || 0) + (jobs['waiting-children'] || 0); }; return ( - -

    Queue Statistics

    + + +

    Queue Statistics

    +
    - {queues.length > 0 ? ( -
    - - - - - - - - - - - - - - - {queues.map((queue) => ( - - - - - - - - - + + {queues.length > 0 ? ( +
    +
    QueueStatusWaitingActiveCompletedFailedDelayedTotal
    {queue.name} - - {queue.jobs.waiting.toLocaleString()} - {queue.jobs.active > 0 ? ( - {queue.jobs.active} - ) : ( - queue.jobs.active - )} - {queue.jobs.completed.toLocaleString()} - {queue.jobs.failed > 0 ? ( - {queue.jobs.failed} - ) : ( - queue.jobs.failed - )} - {queue.jobs.delayed.toLocaleString()}{totalJobs(queue).toLocaleString()}
    + + + + + + + + + + + + + - ))} - -
    QueueStatusWaitingActiveCompletedFailedDelayedPrioritizedChildrenWorkersTotal
    -
    - ) : ( -
    - No queue data available -
    - )} + + + {queues.map((queue) => ( + + {queue.name} + + + + {formatNumber(queue.jobs.waiting)} + + {queue.jobs.active > 0 ? ( + {formatNumber(queue.jobs.active)} + ) : ( + {queue.jobs.active} + )} + + {formatNumber(queue.jobs.completed)} + + {queue.jobs.failed > 0 ? ( + {formatNumber(queue.jobs.failed)} + ) : ( + {queue.jobs.failed} + )} + + {formatNumber(queue.jobs.delayed)} + + {queue.jobs.prioritized && queue.jobs.prioritized > 0 ? ( + {formatNumber(queue.jobs.prioritized)} + ) : ( + 0 + )} + + + {queue.jobs['waiting-children'] && queue.jobs['waiting-children'] > 0 ? ( + {formatNumber(queue.jobs['waiting-children'])} + ) : ( + 0 + )} + + + {queue.workers ? `${queue.workers.count}/${queue.workers.concurrency}` : '-'} + + {formatNumber(totalJobs(queue))} + + ))} + + +
    + ) : ( +
    + No queue data available +
    + )} +
    ); } \ No newline at end of file diff --git a/apps/stock/web-app/src/features/monitoring/components/ServiceCard.tsx b/apps/stock/web-app/src/features/monitoring/components/ServiceCard.tsx new file mode 100644 index 0000000..08e286b --- /dev/null +++ b/apps/stock/web-app/src/features/monitoring/components/ServiceCard.tsx @@ -0,0 +1,56 @@ +import { Card, CardHeader, CardContent } from '@/components/ui/Card'; +import { ServiceStatusIndicator, HealthStatus } from './StatusBadge'; +import { formatUptime } from '../utils/formatters'; +import type { ServiceStatus } from '../types'; + +interface ServiceCardProps { + service: ServiceStatus; +} + +export function ServiceCard({ service }: ServiceCardProps) { + return ( + + +
    +

    + {service.name.replace(/-/g, ' ')} +

    + +
    +
    + + +
    + Status: + {service.status} +
    + +
    + Port: + {service.port || 'N/A'} +
    + +
    + Version: + {service.version} +
    + +
    + Uptime: + {formatUptime(service.uptime)} +
    + +
    + Health: + +
    + + {service.error && ( +
    + Error: {service.error} +
    + )} +
    +
    + ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/monitoring/components/ServiceStatusGrid.tsx b/apps/stock/web-app/src/features/monitoring/components/ServiceStatusGrid.tsx new file mode 100644 index 0000000..fcfaeba --- /dev/null +++ b/apps/stock/web-app/src/features/monitoring/components/ServiceStatusGrid.tsx @@ -0,0 +1,20 @@ +/** + * Service Status Grid Component + */ + +import type { ServiceStatus } from '../types'; +import { ServiceCard } from './ServiceCard'; + +interface ServiceStatusGridProps { + services: ServiceStatus[]; +} + +export function ServiceStatusGrid({ services }: ServiceStatusGridProps) { + return ( +
    + {services.map((service) => ( + + ))} +
    + ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/monitoring/components/StatusBadge.tsx b/apps/stock/web-app/src/features/monitoring/components/StatusBadge.tsx new file mode 100644 index 0000000..353940b --- /dev/null +++ b/apps/stock/web-app/src/features/monitoring/components/StatusBadge.tsx @@ -0,0 +1,80 @@ +import { cn } from '@/lib/utils'; + +type BadgeVariant = 'success' | 'danger' | 'warning' | 'default'; + +interface StatusBadgeProps { + children: React.ReactNode; + variant?: BadgeVariant; + className?: string; + size?: 'sm' | 'md'; +} + +const variantStyles: Record = { + success: 'text-success bg-success/10', + danger: 'text-danger bg-danger/10', + warning: 'text-warning bg-warning/10', + default: 'text-text-secondary bg-text-secondary/10', +}; + +const sizeStyles = { + sm: 'px-2 py-0.5 text-xs', + md: 'px-3 py-1 text-sm', +}; + +export function StatusBadge({ + children, + variant = 'default', + className, + size = 'sm' +}: StatusBadgeProps) { + return ( + + {children} + + ); +} + +interface ConnectionStatusProps { + connected: boolean; + size?: 'sm' | 'md'; +} + +export function ConnectionStatus({ connected, size = 'sm' }: ConnectionStatusProps) { + return ( + + {connected ? 'Connected' : 'Disconnected'} + + ); +} + +interface HealthStatusProps { + healthy: boolean; + size?: 'sm' | 'md'; +} + +export function HealthStatus({ healthy, size = 'sm' }: HealthStatusProps) { + return ( + + {healthy ? 'Healthy' : 'Unhealthy'} + + ); +} + +interface ServiceStatusIndicatorProps { + status: 'running' | 'stopped' | 'error'; +} + +export function ServiceStatusIndicator({ status }: ServiceStatusIndicatorProps) { + const statusColors = { + running: 'bg-success', + stopped: 'bg-text-muted', + error: 'bg-danger', + }; + + return
    ; +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/monitoring/components/SystemHealthCard.tsx b/apps/stock/web-app/src/features/monitoring/components/SystemHealthCard.tsx index 3d2901a..3376f01 100644 --- a/apps/stock/web-app/src/features/monitoring/components/SystemHealthCard.tsx +++ b/apps/stock/web-app/src/features/monitoring/components/SystemHealthCard.tsx @@ -2,75 +2,88 @@ * System Health Card Component */ -import React from 'react'; -import { Card } from '../../../components/ui/Card'; import type { SystemHealth } from '../types'; +import { Card, CardHeader, CardContent } from '@/components/ui/Card'; +import { StatusBadge } from './StatusBadge'; +import { formatUptime, formatBytes, formatPercentage } from '../utils/formatters'; interface SystemHealthCardProps { health: SystemHealth; } export function SystemHealthCard({ health }: SystemHealthCardProps) { - const statusColor = { - healthy: 'text-green-600 bg-green-100', - degraded: 'text-yellow-600 bg-yellow-100', - unhealthy: 'text-red-600 bg-red-100', + const statusVariant = { + healthy: 'success' as const, + degraded: 'warning' as const, + unhealthy: 'danger' as const, }[health.status]; - const formatUptime = (ms: number) => { - const seconds = Math.floor(ms / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - - if (days > 0) return `${days}d ${hours % 24}h`; - if (hours > 0) return `${hours}h ${minutes % 60}m`; - if (minutes > 0) return `${minutes}m ${seconds % 60}s`; - return `${seconds}s`; - }; - - const formatBytes = (bytes: number) => { - const gb = bytes / 1024 / 1024 / 1024; - return gb.toFixed(2) + ' GB'; - }; - return ( - -
    -

    System Health

    - - {health.status.toUpperCase()} - -
    + + +
    +

    System Health

    + + {health.status.toUpperCase()} + +
    +
    -
    +
    -
    Uptime
    -
    {formatUptime(health.uptime)}
    +
    Uptime
    +
    {formatUptime(health.uptime)}
    -
    Memory Usage
    +
    System Memory
    -
    +
    {formatBytes(health.memory.used)} / {formatBytes(health.memory.total)} - {health.memory.percentage.toFixed(1)}% + {formatPercentage(health.memory.percentage)}
    -
    +
    + {health.memory.available !== undefined && ( +
    + Available: {formatBytes(health.memory.available)} (excludes cache/buffers) +
    + )} + {health.memory.heap && ( +
    + Node.js Heap: {formatBytes(health.memory.heap.used)} / {formatBytes(health.memory.heap.total)} +
    + )}
    + {health.cpu && ( +
    +
    CPU Usage
    +
    +
    {health.cpu.usage}%
    + {health.cpu.cores && ( + {health.cpu.cores} cores + )} +
    + {health.cpu.loadAverage && ( +
    + Load: {health.cpu.loadAverage.map(l => l.toFixed(2)).join(', ')} +
    + )} +
    + )} + {health.errors && health.errors.length > 0 && (
    -
    Issues
    +
    Issues
      {health.errors.map((error, index) => ( -
    • +
    • • {error}
    • ))} @@ -78,10 +91,10 @@ export function SystemHealthCard({ health }: SystemHealthCardProps) {
    )} -
    +
    Last updated: {new Date(health.timestamp).toLocaleTimeString()}
    -
    + ); } \ No newline at end of file diff --git a/apps/stock/web-app/src/features/monitoring/components/index.ts b/apps/stock/web-app/src/features/monitoring/components/index.ts index d6c4b60..c94ddfe 100644 --- a/apps/stock/web-app/src/features/monitoring/components/index.ts +++ b/apps/stock/web-app/src/features/monitoring/components/index.ts @@ -5,4 +5,10 @@ export { SystemHealthCard } from './SystemHealthCard'; export { CacheStatsCard } from './CacheStatsCard'; export { QueueStatsTable } from './QueueStatsTable'; -export { DatabaseStatsGrid } from './DatabaseStatsGrid'; \ No newline at end of file +export { DatabaseStatsGrid } from './DatabaseStatsGrid'; +export { ServiceStatusGrid } from './ServiceStatusGrid'; +export { ProxyStatsCard } from './ProxyStatsCard'; +export { StatusBadge, ConnectionStatus, HealthStatus, ServiceStatusIndicator } from './StatusBadge'; +export { MetricCard } from './MetricCard'; +export { ServiceCard } from './ServiceCard'; +export { DatabaseCard } from './DatabaseCard'; \ No newline at end of file diff --git a/apps/stock/web-app/src/features/monitoring/hooks/useMonitoring.ts b/apps/stock/web-app/src/features/monitoring/hooks/useMonitoring.ts index d22cd6d..22ae4f0 100644 --- a/apps/stock/web-app/src/features/monitoring/hooks/useMonitoring.ts +++ b/apps/stock/web-app/src/features/monitoring/hooks/useMonitoring.ts @@ -4,7 +4,15 @@ import { useState, useEffect, useCallback } from 'react'; import { monitoringApi } from '../services/monitoringApi'; -import type { SystemHealth, CacheStats, QueueStats, DatabaseStats } from '../types'; +import type { + SystemHealth, + CacheStats, + QueueStats, + DatabaseStats, + ServiceStatus, + ProxyStats, + SystemOverview +} from '../types'; export function useSystemHealth(refreshInterval: number = 5000) { const [data, setData] = useState(null); @@ -119,5 +127,92 @@ export function useDatabaseStats(refreshInterval: number = 5000) { } }, [fetchData, refreshInterval]); + return { data, loading, error, refetch: fetchData }; +} + +export function useServiceStatus(refreshInterval: number = 5000) { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + const result = await monitoringApi.getServiceStatus(); + setData(result.services); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch service status'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + + if (refreshInterval > 0) { + const interval = setInterval(fetchData, refreshInterval); + return () => clearInterval(interval); + } + }, [fetchData, refreshInterval]); + + return { data, loading, error, refetch: fetchData }; +} + +export function useProxyStats(refreshInterval: number = 5000) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + const stats = await monitoringApi.getProxyStats(); + setData(stats); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch proxy stats'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + + if (refreshInterval > 0) { + const interval = setInterval(fetchData, refreshInterval); + return () => clearInterval(interval); + } + }, [fetchData, refreshInterval]); + + return { data, loading, error, refetch: fetchData }; +} + +export function useSystemOverview(refreshInterval: number = 5000) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + const overview = await monitoringApi.getSystemOverview(); + setData(overview); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch system overview'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + + if (refreshInterval > 0) { + const interval = setInterval(fetchData, refreshInterval); + return () => clearInterval(interval); + } + }, [fetchData, refreshInterval]); + return { data, loading, error, refetch: fetchData }; } \ No newline at end of file diff --git a/apps/stock/web-app/src/features/monitoring/services/monitoringApi.ts b/apps/stock/web-app/src/features/monitoring/services/monitoringApi.ts index 52b2581..2e3e210 100644 --- a/apps/stock/web-app/src/features/monitoring/services/monitoringApi.ts +++ b/apps/stock/web-app/src/features/monitoring/services/monitoringApi.ts @@ -2,7 +2,15 @@ * Monitoring API Service */ -import type { SystemHealth, CacheStats, QueueStats, DatabaseStats } from '../types'; +import type { + SystemHealth, + CacheStats, + QueueStats, + DatabaseStats, + ServiceStatus, + ProxyStats, + SystemOverview +} from '../types'; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:2003'; const MONITORING_BASE = `${API_BASE_URL}/api/system/monitoring`; @@ -84,4 +92,37 @@ export const monitoringApi = { } return response.json(); }, + + /** + * Get service status + */ + async getServiceStatus(): Promise<{ services: ServiceStatus[] }> { + const response = await fetch(`${MONITORING_BASE}/services`); + if (!response.ok) { + throw new Error(`Failed to fetch service status: ${response.statusText}`); + } + return response.json(); + }, + + /** + * Get proxy statistics + */ + async getProxyStats(): Promise { + const response = await fetch(`${MONITORING_BASE}/proxies`); + if (!response.ok) { + throw new Error(`Failed to fetch proxy stats: ${response.statusText}`); + } + return response.json(); + }, + + /** + * Get system overview + */ + async getSystemOverview(): Promise { + const response = await fetch(`${MONITORING_BASE}/overview`); + if (!response.ok) { + throw new Error(`Failed to fetch system overview: ${response.statusText}`); + } + return response.json(); + }, }; \ No newline at end of file diff --git a/apps/stock/web-app/src/features/monitoring/types/index.ts b/apps/stock/web-app/src/features/monitoring/types/index.ts index 9365e44..edfcd1a 100644 --- a/apps/stock/web-app/src/features/monitoring/types/index.ts +++ b/apps/stock/web-app/src/features/monitoring/types/index.ts @@ -31,6 +31,8 @@ export interface QueueStats { failed: number; delayed: number; paused: number; + prioritized?: number; + 'waiting-children'?: number; }; workers?: { count: number; @@ -66,10 +68,16 @@ export interface SystemHealth { used: number; total: number; percentage: number; + available?: number; + heap?: { + used: number; + total: number; + }; }; cpu?: { usage: number; loadAverage?: number[]; + cores?: number; }; services: { cache: CacheStats; @@ -77,4 +85,36 @@ export interface SystemHealth { databases: DatabaseStats[]; }; errors?: string[]; +} + +export interface ServiceStatus { + name: string; + version: string; + status: 'running' | 'stopped' | 'error'; + port?: number; + uptime: number; + lastCheck: string; + healthy: boolean; + error?: string; +} + +export interface ProxyStats { + enabled: boolean; + totalProxies: number; + workingProxies: number; + failedProxies: number; + lastUpdate?: string; + lastFetchTime?: string; +} + +export interface SystemOverview { + services: ServiceStatus[]; + health: SystemHealth; + proxies?: ProxyStats; + environment: { + nodeVersion: string; + platform: string; + architecture: string; + hostname: string; + }; } \ No newline at end of file diff --git a/apps/stock/web-app/src/features/monitoring/utils/formatters.ts b/apps/stock/web-app/src/features/monitoring/utils/formatters.ts new file mode 100644 index 0000000..e666731 --- /dev/null +++ b/apps/stock/web-app/src/features/monitoring/utils/formatters.ts @@ -0,0 +1,42 @@ +/** + * Common formatting utilities for monitoring components + */ + +export function formatUptime(ms: number): string { + const seconds = Math.floor(ms / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (days > 0) return `${days}d ${hours % 24}h`; + if (hours > 0) return `${hours}h ${minutes % 60}m`; + if (minutes > 0) return `${minutes}m ${seconds % 60}s`; + return `${seconds}s`; +} + +export function formatBytes(bytes: number): string { + const gb = bytes / 1024 / 1024 / 1024; + if (gb >= 1) { + return gb.toFixed(2) + ' GB'; + } + + const mb = bytes / 1024 / 1024; + if (mb >= 1) { + return mb.toFixed(2) + ' MB'; + } + + const kb = bytes / 1024; + if (kb >= 1) { + return kb.toFixed(2) + ' KB'; + } + + return bytes + ' B'; +} + +export function formatNumber(num: number): string { + return num.toLocaleString(); +} + +export function formatPercentage(value: number, decimals: number = 1): string { + return `${value.toFixed(decimals)}%`; +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/pipeline/PipelinePage.tsx b/apps/stock/web-app/src/features/pipeline/PipelinePage.tsx index 2e69dfa..1adf1df 100644 --- a/apps/stock/web-app/src/features/pipeline/PipelinePage.tsx +++ b/apps/stock/web-app/src/features/pipeline/PipelinePage.tsx @@ -5,7 +5,6 @@ import { CloudArrowDownIcon, ExclamationTriangleIcon, CheckCircleIcon, - ClockIcon, } from '@heroicons/react/24/outline'; import { usePipeline } from './hooks/usePipeline'; import type { PipelineOperation } from './types'; @@ -145,11 +144,11 @@ export function PipelinePage() { const getCategoryIcon = (category: string) => { switch (category) { case 'sync': - return ; + return ; case 'maintenance': - return ; + return ; default: - return ; + return ; } }; @@ -165,21 +164,21 @@ export function PipelinePage() { }; return ( -
    -
    -

    Data Pipeline Management

    -

    +

    +
    +

    Data Pipeline Management

    +

    Manage data synchronization and maintenance operations

    {/* Stats Overview */} {(stats.exchanges || stats.providerMappings) && ( -
    +
    {stats.exchanges && ( -
    -

    Exchange Statistics

    -
    +
    +

    Exchange Statistics

    +
    Total Exchanges: {stats.exchanges.totalExchanges} @@ -201,9 +200,9 @@ export function PipelinePage() { )} {stats.providerMappings && ( -
    -

    Provider Mapping Statistics

    -
    +
    +

    Provider Mapping Statistics

    +
    Coverage: @@ -224,7 +223,7 @@ export function PipelinePage() {
    {Object.entries(stats.providerMappings.mappingsByProvider).map(([provider, count]) => ( - {provider}: {count} + {provider}: {String(count)} ))}
    @@ -238,27 +237,27 @@ export function PipelinePage() { {/* Status Messages */} {error && ( -
    +
    - - {error} + + {error}
    )} {lastJobResult && ( -
    {lastJobResult.success ? ( - + ) : ( - + )} - + {lastJobResult.message || lastJobResult.error} {lastJobResult.jobId && ( @@ -271,35 +270,35 @@ export function PipelinePage() { )} {/* Operations Grid */} -
    +
    {/* Sync Operations */}
    -

    - +

    + Sync Operations

    -
    +
    {operations.filter(op => op.category === 'sync').map(op => (
    -

    {op.name}

    +

    {op.name}

    {getCategoryIcon(op.category)}
    -

    {op.description}

    +

    {op.description}

    {/* Special inputs for specific operations */} {op.id === 'sync-provider-symbols' && ( -
    +
    handleOperation(op)} disabled={loading} - className="w-full px-3 py-2 bg-primary-500/20 text-primary-400 rounded text-sm font-medium hover:bg-primary-500/30 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center gap-2" + className="w-full px-2.5 py-1.5 bg-primary-500/20 text-primary-400 rounded text-xs font-medium hover:bg-primary-500/30 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center gap-2" > {loading ? ( <> - + Processing... ) : ( <> - + Execute )} @@ -346,31 +345,31 @@ export function PipelinePage() { {/* Maintenance Operations */}
    -

    - +

    + Maintenance Operations

    -
    +
    {operations.filter(op => op.category === 'maintenance').map(op => (
    -

    {op.name}

    +

    {op.name}

    {getCategoryIcon(op.category)}
    -

    {op.description}

    +

    {op.description}

    {op.id === 'clear-postgresql' && ( -
    +