diff --git a/application/src/components/servers/ServerHistoryCharts.tsx b/application/src/components/servers/ServerHistoryCharts.tsx index a53371a..b9b0cce 100644 --- a/application/src/components/servers/ServerHistoryCharts.tsx +++ b/application/src/components/servers/ServerHistoryCharts.tsx @@ -1,20 +1,23 @@ import { useQuery } from "@tanstack/react-query"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; -import { LineChart, Line, XAxis, YAxis, CartesianGrid, ResponsiveContainer } from "recharts"; +import { LineChart, Line, AreaChart, Area, XAxis, YAxis, CartesianGrid, ResponsiveContainer } from "recharts"; import { serverService } from "@/services/serverService"; import { Loader2, TrendingUp, Cpu, HardDrive, Wifi, MemoryStick } from "lucide-react"; import { useTheme } from "@/contexts/ThemeContext"; -import { DateRangeFilter } from "@/components/services/DateRangeFilter"; +import { TimeRangeSelector } from "./charts/TimeRangeSelector"; import { useState } from "react"; +import { formatChartData, filterMetricsByTimeRange, formatBytes } from "./charts/dataUtils"; interface ServerHistoryChartsProps { serverId: string; } +type TimeRange = '60m' | '1d' | '7d' | '1m' | '3m'; + export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => { const { theme } = useTheme(); - const [dateRange, setDateRange] = useState({ start: new Date(Date.now() - 24 * 60 * 60 * 1000), end: new Date() }); + const [timeRange, setTimeRange] = useState("1d"); console.log('ServerHistoryCharts: Rendering with serverId:', serverId); @@ -23,117 +26,135 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => { isLoading, error } = useQuery({ - queryKey: ['server-metrics-history', serverId], + queryKey: ['server-metrics-history', serverId, timeRange], queryFn: async () => { console.log('ServerHistoryCharts: Fetching metrics for serverId:', serverId); - const result = await serverService.getServerMetrics(serverId); + const result = await serverService.getServerMetrics(serverId, timeRange); console.log('ServerHistoryCharts: Raw metrics result:', result); return result; }, enabled: !!serverId, - refetchInterval: 60000, // Refresh every minute + refetchInterval: 60000, retry: 1 }); - const handleDateRangeChange = (startDate: Date, endDate: Date, option: any) => { - setDateRange({ start: startDate, end: endDate }); - console.log('Date range changed:', { startDate, endDate, option }); - }; - console.log('ServerHistoryCharts: Query state:', { metricsCount: metrics.length, isLoading, error: error?.message, firstMetric: metrics[0], - serverId + serverId, + timeRange }); - const formatBytes = (bytes: number) => { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; - }; - - const parseValueWithUnit = (value: string | number) => { - if (typeof value === 'number') return value; - const match = value.toString().match(/^([\d.]+)/); - return match ? parseFloat(match[1]) : 0; - }; - - const convertToBytes = (value: string | number): number => { - if (typeof value === 'number') return value; - - const str = value.toString(); - const numMatch = str.match(/^([\d.]+)/); - const unitMatch = str.match(/([A-Za-z]+)$/); - - if (!numMatch) return 0; - - const num = parseFloat(numMatch[1]); - const unit = unitMatch ? unitMatch[1].toUpperCase() : 'B'; - - const multipliers: { [key: string]: number } = { - 'B': 1, - 'KB': 1024, - 'MB': 1024 * 1024, - 'GB': 1024 * 1024 * 1024, - 'TB': 1024 * 1024 * 1024 * 1024 - }; - - return num * (multipliers[unit] || 1); - }; - - const formatChartData = (metrics: any[]) => { - console.log('ServerHistoryCharts: Formatting chart data for', metrics.length, 'metrics'); - - if (!Array.isArray(metrics) || metrics.length === 0) { - console.log('ServerHistoryCharts: No metrics to format'); - return []; - } - - const formattedData = metrics.slice(-50).reverse().map((metric, index) => { - console.log(`ServerHistoryCharts: Processing metric ${index}:`, metric); - - const cpuUsage = parseValueWithUnit(metric.cpu_usage || 0); - const ramUsedBytes = convertToBytes(metric.ram_used || 0); - const ramTotalBytes = convertToBytes(metric.ram_total || 0); - const diskUsedBytes = convertToBytes(metric.disk_used || 0); - const diskTotalBytes = convertToBytes(metric.disk_total || 0); - - const ramUsagePercent = ramTotalBytes > 0 ? (ramUsedBytes / ramTotalBytes) * 100 : 0; - const diskUsagePercent = diskTotalBytes > 0 ? (diskUsedBytes / diskTotalBytes) * 100 : 0; - - return { - timestamp: new Date(metric.timestamp || metric.created).toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit' - }), - fullTimestamp: new Date(metric.timestamp || metric.created).toLocaleString(), - cpuUsage: Math.round(cpuUsage * 100) / 100, - ramUsagePercent: Math.round(ramUsagePercent * 100) / 100, - diskUsagePercent: Math.round(diskUsagePercent * 100) / 100, - ramUsedBytes, - ramTotalBytes, - diskUsedBytes, - diskTotalBytes, - networkRxBytes: metric.network_rx_bytes || 0, - networkTxBytes: metric.network_tx_bytes || 0, - networkRxSpeed: parseValueWithUnit(metric.network_rx_speed || 0), - networkTxSpeed: parseValueWithUnit(metric.network_tx_speed || 0), - }; - }); - - console.log('ServerHistoryCharts: Formatted chart data:', formattedData); - return formattedData; - }; - - const chartData = formatChartData(metrics); + const chartData = formatChartData(metrics, timeRange); const getGridColor = () => theme === 'dark' ? '#374151' : '#e5e7eb'; const getAxisColor = () => theme === 'dark' ? '#9ca3af' : '#6b7280'; + // Custom tooltip content with detailed information and full date/time + const DetailedTooltipContent = ({ active, payload, label }: any) => { + if (active && payload && payload.length) { + const data = payload[0]?.payload; + return ( +
+

{label}

+ {data?.fullTimestamp && ( +

+ {new Date(data.fullTimestamp).toLocaleString('en-US', { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + })} +

+ )} + {payload.map((entry: any, index: number) => { + const data = entry.payload; + return ( +
+
+
+ {entry.name}: {entry.value}% +
+ {entry.dataKey === 'cpuUsage' && ( +
+
CPU Cores: {data.cpuCores}
+
Free: {data.cpuFree}%
+
+ )} + {entry.dataKey === 'ramUsagePercent' && ( +
+
Used: {data.ramUsed}
+
Total: {data.ramTotal}
+
Free: {data.ramFree}
+
+ )} + {entry.dataKey === 'diskUsagePercent' && ( +
+
Used: {data.diskUsed}
+
Total: {data.diskTotal}
+
Free: {data.diskFree}
+
+ )} +
+ ); + })} +
+ ); + } + return null; + }; + + // Network tooltip with detailed info and full date/time + const NetworkTooltipContent = ({ active, payload, label }: any) => { + if (active && payload && payload.length) { + const data = payload[0]?.payload; + return ( +
+

{label}

+ {data?.fullTimestamp && ( +

+ {new Date(data.fullTimestamp).toLocaleString('en-US', { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + })} +

+ )} + {payload.map((entry: any, index: number) => ( +
+
+ + {entry.name}: {entry.dataKey.includes('Speed') ? `${entry.value} KB/s` : formatBytes(entry.value)} + +
+ ))} + {data && ( +
+
Total RX: {data.networkRx}
+
Total TX: {data.networkTx}
+
+ )} +
+ ); + } + return null; + }; + if (isLoading) { return (
@@ -143,6 +164,7 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {

Historical Performance

+
{[1, 2, 3, 4].map((index) => ( @@ -174,7 +196,7 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {

Historical Performance

- +
@@ -197,16 +219,16 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {

Historical Performance

- +
-

No historical data available

-

Metrics count: {metrics.length}

+

No historical data available for {timeRange}

+

Raw metrics count: {metrics.length}

Server ID: {serverId}

- {metrics.length > 0 ? 'Data exists but failed to format' : 'No metrics data found'} + {metrics.length > 0 ? 'Data exists but filtered out by time range' : 'No metrics data found'}

@@ -215,7 +237,10 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => { ); } - console.log('ServerHistoryCharts: Rendering individual charts with', chartData.length, 'data points'); + console.log('ServerHistoryCharts: Rendering charts with', chartData.length, 'data points for time range:', timeRange); + + // Calculate summary stats from latest data point + const latestData = chartData[chartData.length - 1]; return (
@@ -223,20 +248,28 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {

Historical Performance

- ({chartData.length} data points) + ({chartData.length} data points • {timeRange})
- +
- {/* CPU Usage Chart */} + {/* CPU Usage Chart - Smooth Area Chart with Blue Gradient */} - -
- + +
+
+ +
+ CPU Usage
- CPU Usage + {latestData && ( +
+
{latestData.cpuUsage}%
+
{latestData.cpuCores} cores
+
+ )}
@@ -246,10 +279,10 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => { color: theme === 'dark' ? "#3b82f6" : "#2563eb", } }} className="h-80"> - + - + @@ -266,10 +299,10 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => { label={{ value: 'CPU %', angle: -90, position: 'insideLeft' }} /> } + content={} cursor={{ stroke: getGridColor() }} /> - { fillOpacity={1} name="CPU Usage (%)" /> - + - {/* Memory Usage Chart */} + {/* Memory Usage Chart - Basis Area Chart with Green Gradient */} - -
- + +
+
+ +
+ Memory Usage
- Memory Usage + {latestData && ( +
+
{latestData.ramUsagePercent}%
+
{latestData.ramUsed} / {latestData.ramTotal}
+
+ )}
@@ -301,10 +342,10 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => { color: theme === 'dark' ? "#10b981" : "#059669", } }} className="h-80"> - + - + @@ -321,11 +362,11 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => { label={{ value: 'Memory %', angle: -90, position: 'insideLeft' }} /> } + content={} cursor={{ stroke: getGridColor() }} /> - { fillOpacity={1} name="Memory Usage (%)" /> - + - {/* Disk Usage Chart */} + {/* Disk Usage Chart - Stepped Area Chart with Orange Gradient */} - -
- + +
+
+ +
+ Disk Usage
- Disk Usage + {latestData && ( +
+
{latestData.diskUsagePercent}%
+
{latestData.diskUsed} / {latestData.diskTotal}
+
+ )}
@@ -356,10 +405,10 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => { color: theme === 'dark' ? "#f59e0b" : "#d97706", } }} className="h-80"> - + - + @@ -376,11 +425,11 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => { label={{ value: 'Disk %', angle: -90, position: 'insideLeft' }} /> } + content={} cursor={{ stroke: getGridColor() }} /> - { fillOpacity={1} name="Disk Usage (%)" /> - + - {/* Network Traffic Chart */} + {/* Network Traffic Chart - Dual Line Chart with Purple/Red Theme */} - -
- + +
+
+ +
+ Network Traffic
- Network Traffic + {latestData && ( +
+
{latestData.networkRxSpeed} KB/s ↓
+
{latestData.networkTxSpeed} KB/s ↑
+
+ )}
@@ -421,6 +478,13 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => { + + + + + + + { formatBytes(value) + '/s'} - label={{ value: 'Network Speed', angle: -90, position: 'insideLeft' }} + label={{ value: 'Network Speed (KB/s)', angle: -90, position: 'insideLeft' }} /> } + content={} cursor={{ stroke: getGridColor() }} - formatter={(value, name) => [formatBytes(Number(value)) + '/s', name]} /> { stroke={theme === 'dark' ? "#a78bfa" : "#7c3aed"} strokeWidth={3} dot={false} - fill="url(#networkGradient)" - fillOpacity={0.5} name="RX Speed" + filter="url(#glow)" + strokeDasharray="0" /> { strokeWidth={3} dot={false} name="TX Speed" + strokeDasharray="5 5" /> diff --git a/application/src/components/servers/ServerMetricsCharts.tsx b/application/src/components/servers/ServerMetricsCharts.tsx index 70dd5be..31fc7e0 100644 --- a/application/src/components/servers/ServerMetricsCharts.tsx +++ b/application/src/components/servers/ServerMetricsCharts.tsx @@ -26,7 +26,7 @@ export const ServerMetricsCharts = ({ serverId }: ServerMetricsChartsProps) => { error } = useQuery({ queryKey: ['server-metrics', serverId, timeRange], - queryFn: () => serverService.getServerMetrics(serverId), + queryFn: () => serverService.getServerMetrics(serverId, timeRange), enabled: !!serverId, refetchInterval: 30000 }); @@ -53,7 +53,7 @@ export const ServerMetricsCharts = ({ serverId }: ServerMetricsChartsProps) => { if (chartData.length === 0) { return (
-

No server metrics data available

+

No server metrics data available for {timeRange}

); } @@ -63,7 +63,7 @@ export const ServerMetricsCharts = ({ serverId }: ServerMetricsChartsProps) => {

Server Metrics

- ({chartData.length} data points) + ({chartData.length} data points • {timeRange})
diff --git a/application/src/components/servers/charts/dataUtils.ts b/application/src/components/servers/charts/dataUtils.ts index 92f10f5..5e0ef91 100644 --- a/application/src/components/servers/charts/dataUtils.ts +++ b/application/src/components/servers/charts/dataUtils.ts @@ -57,11 +57,43 @@ export const filterMetricsByTimeRange = (metrics: any[], timeRange: TimeRange): const cutoffTime = new Date(now.getTime() - (selectedRange.hours * 60 * 60 * 1000)); return metrics.filter(metric => { - const metricTime = new Date(metric.timestamp); + const metricTime = new Date(metric.created || metric.timestamp); return metricTime >= cutoffTime; }); }; +const formatTimestamp = (timestamp: string, timeRange: TimeRange): string => { + const date = new Date(timestamp); + + if (timeRange === '60m') { + // For 60 minutes, show time with seconds + return date.toLocaleString('en-US', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + } else if (timeRange === '1d') { + // For 1 day, show date and time + return date.toLocaleString('en-US', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } else { + // For longer periods, show date and time + return date.toLocaleString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } +}; + export const formatChartData = (metrics: any[], timeRange: TimeRange) => { const filteredMetrics = filterMetricsByTimeRange(metrics, timeRange); @@ -85,11 +117,11 @@ export const formatChartData = (metrics: any[], timeRange: TimeRange) => { const networkRxSpeed = metric.network_rx_speed || 0; const networkTxSpeed = metric.network_tx_speed || 0; + const timestamp = metric.created || metric.timestamp; + return { - timestamp: new Date(metric.timestamp).toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit' - }), + timestamp: formatTimestamp(timestamp, timeRange), + fullTimestamp: timestamp, cpuUsage: Math.round(cpuUsage * 100) / 100, cpuCores: parseInt(metric.cpu_cores) || 0, cpuFree: 100 - cpuUsage, diff --git a/application/src/services/serverService.ts b/application/src/services/serverService.ts index e95d9d7..eb2b2d7 100644 --- a/application/src/services/serverService.ts +++ b/application/src/services/serverService.ts @@ -1,4 +1,3 @@ - import { pb } from '@/lib/pocketbase'; import { Server, ServerStats } from '@/types/server.types'; @@ -23,9 +22,9 @@ export const serverService = { } }, - async getServerMetrics(serverId: string): Promise { + async getServerMetrics(serverId: string, timeRange?: string): Promise { try { - console.log('serverService.getServerMetrics: Fetching metrics for serverId:', serverId); + console.log('serverService.getServerMetrics: Fetching metrics for serverId:', serverId, 'timeRange:', timeRange); // First, get the server to find the correct server_id for metrics let server; @@ -36,48 +35,63 @@ export const serverService = { console.log('serverService.getServerMetrics: Could not fetch server details:', error); } - // Try to get metrics using the server's server_id field if available + // Use the server's server_id field if available, otherwise use the serverId let metricsServerId = serverId; if (server && server.server_id) { metricsServerId = server.server_id; console.log('serverService.getServerMetrics: Using server.server_id for metrics:', metricsServerId); } - // Try filtering by server_id first - let filteredRecords = await pb.collection('server_metrics').getFullList({ - filter: `server_id = "${metricsServerId}"`, + // Build filter for server_id and time range + let filter = `server_id = "${metricsServerId}"`; + + // Add agent_id filter if available in server data + if (server && server.agent_id) { + filter += ` && agent_id = "${server.agent_id}"`; + console.log('serverService.getServerMetrics: Added agent_id filter:', server.agent_id); + } + + // Add time range filter + if (timeRange) { + const now = new Date(); + let cutoffTime; + + switch (timeRange) { + case '60m': + cutoffTime = new Date(now.getTime() - (60 * 60 * 1000)); + break; + case '1d': + cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000)); + break; + case '7d': + cutoffTime = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000)); + break; + case '1m': + cutoffTime = new Date(now.getTime() - (30 * 24 * 60 * 60 * 1000)); + break; + case '3m': + cutoffTime = new Date(now.getTime() - (90 * 24 * 60 * 60 * 1000)); + break; + default: + cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000)); + } + + const cutoffISO = cutoffTime.toISOString(); + filter += ` && created >= "${cutoffISO}"`; + console.log('serverService.getServerMetrics: Using time filter:', cutoffISO); + } + + console.log('serverService.getServerMetrics: Final filter:', filter); + + // Fetch filtered records with proper sorting + const records = await pb.collection('server_metrics').getFullList({ + filter: filter, sort: '-created', requestKey: null }); - console.log('serverService.getServerMetrics: Filtered records by server_id:', filteredRecords.length); - - // If no records found with server_id, try alternative approaches - if (filteredRecords.length === 0) { - console.log('serverService.getServerMetrics: No records found with server_id filter, trying alternatives...'); - - // Get all records to see what's available - const allRecords = await pb.collection('server_metrics').getFullList({ - sort: '-created', - requestKey: null - }); - - console.log('serverService.getServerMetrics: Total server_metrics records:', allRecords.length); - if (allRecords.length > 0) { - console.log('serverService.getServerMetrics: Sample record fields:', Object.keys(allRecords[0])); - console.log('serverService.getServerMetrics: Sample server_id values:', allRecords.slice(0, 5).map(r => r.server_id)); - } - - // For now, return some sample data from available records if server matches pattern - // This is temporary until the correct server_id mapping is established - if (allRecords.length > 0) { - console.log('serverService.getServerMetrics: Using available records as fallback'); - filteredRecords = allRecords.slice(0, 50); // Get recent 50 records - } - } - - console.log('serverService.getServerMetrics: Returning', filteredRecords.length, 'records'); - return filteredRecords; + console.log('serverService.getServerMetrics: Found', records.length, 'records with filter'); + return records; } catch (error) { console.error('Error fetching server metrics:', error); throw error;