Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,4 @@ deadlock-analysis.md
# AI
.AGENT
graphify-out
coderabbitai.md
8 changes: 4 additions & 4 deletions dashboard/src/app/router.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Suspense } from 'react'
import { useAdmin } from '@/hooks/use-admin'
import { getCurrentAdmin } from '@/service/api'
import { getCurrentAdmin, type AdminDetails } from '@/service/api'
import { hasPermission } from '@/utils/rbac'
import { createHashRouter, Navigate, RouteObject } from 'react-router'
import { createHashRouter, Navigate, type RouteObject } from 'react-router'
import { LoadingSpinner } from '@/components/common/loading-spinner'
import { TabbedRouteSuspenseFallback } from '@/components/layout/tabbed-route-suspense-fallback'
import { lazyWithChunkRecovery } from '@/utils/chunk-recovery'
Expand Down Expand Up @@ -59,7 +59,7 @@ function TemplatesIndex() {
return <Navigate to={defaultPath} replace />
}

const fetchAdminLoader = async (): Promise<any> => {
const fetchAdminLoader = async (): Promise<AdminDetails> => {
try {
const response = await getCurrentAdmin()
return response
Expand Down Expand Up @@ -392,4 +392,4 @@ export const router = createHashRouter([
</Suspense>
),
},
] as RouteObject[])
] satisfies RouteObject[])
26 changes: 18 additions & 8 deletions dashboard/src/components/charts/all-nodes-stacked-bar-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { type ChartConfig, ChartContainer, ChartTooltip } from '@/components/ui/chart'
import { useTranslation } from 'react-i18next'
import useDirDetection from '@/hooks/use-dir-detection'
import { useDocumentVisibility } from '@/hooks/use-document-visibility'
import { useChartViewType } from '@/hooks/use-chart-view-type'
import { Period, type NodeUsageStat, type UserUsageStat, useGetAdminUsageById, useGetAdminUsageByUsername, useGetNodesSimple, type NodeSimple, useGetUsage } from '@/service/api'
import { formatBytes, formatGigabytes } from '@/utils/formatByte'
Expand Down Expand Up @@ -84,8 +85,15 @@ const getStackedNodeRadius = (row: NodeChartDataPoint, nodeName: string, nodeLis
return [isTopSegment ? STACKED_BAR_RADIUS : 0, isTopSegment ? STACKED_BAR_RADIUS : 0, isBottomSegment ? STACKED_BAR_RADIUS : 0, isBottomSegment ? STACKED_BAR_RADIUS : 0]
}

function CustomTooltip({ active, payload, chartConfig, dir, period }: TooltipProps<number, string> & { chartConfig?: ChartConfig; dir: string; period: Period }) {
const { t, i18n } = useTranslation()
function CustomTooltip(
{ active, payload, chartConfig, dir, period, t, locale }: TooltipProps<number, string> & {
chartConfig?: ChartConfig
dir: string
period: Period
t: (key: string, options?: Record<string, unknown>) => string
locale: string
},
) {
const [isMobile, setIsMobile] = useState(false)

useEffect(() => {
Expand All @@ -100,7 +108,7 @@ function CustomTooltip({ active, payload, chartConfig, dir, period }: TooltipPro
if (!active || !payload || !payload.length) return null

const data = payload[0].payload as NodeChartDataPoint
const formattedDate = data._period_start ? formatTooltipDate(data._period_start, period, i18n.language) : String(data.time || '')
const formattedDate = data._period_start ? formatTooltipDate(data._period_start, period, locale) : String(data.time || '')

const getNodeColor = (nodeName: string) => chartConfig?.[nodeName]?.color || 'hsl(var(--chart-1))'
const isRTL = dir === 'rtl'
Expand Down Expand Up @@ -217,9 +225,11 @@ export function AllNodesStackedBarChart() {
const { t, i18n } = useTranslation()
const dir = useDirDetection()
const chartViewType = useChartViewType()
const isTabVisible = useDocumentVisibility()
const { data: nodesResponse } = useGetNodesSimple({ all: true }, { query: { enabled: true } })
const { resolvedTheme } = useTheme()
const shouldUseNodeUsage = selectedAdmin === 'all'
const pollingInterval = isTabVisible ? 1000 * 60 * 15 : 1000 * 60 * 60

const handleModalNavigate = (index: number) => {
if (!chartData[index]) return
Expand Down Expand Up @@ -285,7 +295,7 @@ export function AllNodesStackedBarChart() {
} = useGetUsage(usageParams, {
query: {
enabled: shouldUseNodeUsage,
refetchInterval: 1000 * 60 * 5,
refetchInterval: pollingInterval,
},
})

Expand All @@ -296,7 +306,7 @@ export function AllNodesStackedBarChart() {
} = useGetAdminUsageById(selectedAdminId ?? 0, usageParams, {
query: {
enabled: !shouldUseNodeUsage && selectedAdmin !== 'all' && selectedAdminId != null,
refetchInterval: 1000 * 60 * 5,
refetchInterval: pollingInterval,
},
})

Expand All @@ -307,7 +317,7 @@ export function AllNodesStackedBarChart() {
} = useGetAdminUsageByUsername(selectedAdmin, usageParams, {
query: {
enabled: !shouldUseNodeUsage && selectedAdmin !== 'all' && selectedAdminId == null,
refetchInterval: 1000 * 60 * 5,
refetchInterval: pollingInterval,
},
})

Expand Down Expand Up @@ -623,7 +633,7 @@ export function AllNodesStackedBarChart() {
width={32}
tickMargin={2}
/>
<ChartTooltip cursor={false} content={props => <CustomTooltip {...(props as TooltipProps<number, string>)} chartConfig={chartConfig} dir={dir} period={activePeriod} />} />
<ChartTooltip cursor={false} content={props => <CustomTooltip {...(props as TooltipProps<number, string>)} chartConfig={chartConfig} dir={dir} period={activePeriod} t={t} locale={i18n.language} />} />
{nodeList.map((node, index) => (
<Area
key={node.id}
Expand Down Expand Up @@ -656,7 +666,7 @@ export function AllNodesStackedBarChart() {
width={32}
tickMargin={2}
/>
<ChartTooltip cursor={false} content={props => <CustomTooltip {...(props as TooltipProps<number, string>)} chartConfig={chartConfig} dir={dir} period={activePeriod} />} />
<ChartTooltip cursor={false} content={props => <CustomTooltip {...(props as TooltipProps<number, string>)} chartConfig={chartConfig} dir={dir} period={activePeriod} t={t} locale={i18n.language} />} />
{nodeList.map((node, index) => (
<Bar key={node.id} dataKey={node.name} stackId="a" fill={chartConfig[node.name]?.color || `hsl(var(--chart-${(index % 5) + 1}))`} radius={SQUARE_STACK_RADIUS} cursor="pointer">
{chartData.map(row => (
Expand Down
4 changes: 3 additions & 1 deletion dashboard/src/components/charts/area-costume-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
toChartQueryEndDate,
} from '@/utils/chart-period-utils'
import useDirDetection from '@/hooks/use-dir-detection'
import { useDocumentVisibility } from '@/hooks/use-document-visibility'

type DataPoint = {
time: string
Expand Down Expand Up @@ -75,6 +76,7 @@ export function AreaCostumeChart({ nodeId, currentStats, realtimeStats, realtime
const { t, i18n } = useTranslation()
const { resolvedTheme } = useTheme()
const dir = useDirDetection()
const isTabVisible = useDocumentVisibility()
const [realtimeHistory, setRealtimeHistory] = useState<DataPoint[]>([])
const [realtimeError, setRealtimeError] = useState<Error | null>(null)
const [viewMode, setViewMode] = useState<'realtime' | 'historical'>(() => (realtimeAvailable ? 'realtime' : 'historical'))
Expand Down Expand Up @@ -208,7 +210,7 @@ export function AreaCostumeChart({ nodeId, currentStats, realtimeStats, realtime
} = useGetNodeStatsPeriodic(nodeId ?? 0, historicalParams, {
query: {
enabled: viewMode === 'historical' && nodeId !== undefined,
refetchInterval: 1000 * 60 * 5,
refetchInterval: isTabVisible ? 1000 * 60 * 15 : 1000 * 60 * 60,
},
})

Expand Down
11 changes: 8 additions & 3 deletions dashboard/src/components/charts/costume-bar-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { type ChartConfig, ChartContainer, ChartTooltip } from '@/components/ui/chart'
import { useTranslation } from 'react-i18next'
import useDirDetection from '@/hooks/use-dir-detection'
import { useDocumentVisibility } from '@/hooks/use-document-visibility'
import { useChartViewType } from '@/hooks/use-chart-view-type'
import { Period, type NodeUsageStat, type UserUsageStat, useGetAdminUsageById, useGetAdminUsageByUsername, useGetUsage } from '@/service/api'
import { formatBytes } from '@/utils/formatByte'
Expand Down Expand Up @@ -118,8 +119,12 @@ export function CostumeBarChart({ nodeId }: CostumeBarChartProps) {
const { t, i18n } = useTranslation()
const dir = useDirDetection()
const chartViewType = useChartViewType()
const isTabVisible = useDocumentVisibility()
const shouldUseNodeUsage = selectedAdmin === 'all'

// When tab is hidden, reduce polling frequency to save VPS resources
const pollingInterval = isTabVisible ? 1000 * 60 * 15 : 1000 * 60 * 60

const activeQueryRange = useMemo(() => {
if (showCustomRange && customRange?.from && customRange?.to) {
return getChartQueryRangeFromDateRange(customRange, selectedTime)
Expand Down Expand Up @@ -157,7 +162,7 @@ export function CostumeBarChart({ nodeId }: CostumeBarChartProps) {
} = useGetUsage(nodeUsageParams, {
query: {
enabled: shouldUseNodeUsage,
refetchInterval: 1000 * 60 * 5,
refetchInterval: pollingInterval,
},
})

Expand All @@ -168,7 +173,7 @@ export function CostumeBarChart({ nodeId }: CostumeBarChartProps) {
} = useGetAdminUsageById(selectedAdminId ?? 0, adminUsageParams, {
query: {
enabled: !shouldUseNodeUsage && selectedAdmin !== 'all' && selectedAdminId != null,
refetchInterval: 1000 * 60 * 5,
refetchInterval: pollingInterval,
},
})

Expand All @@ -179,7 +184,7 @@ export function CostumeBarChart({ nodeId }: CostumeBarChartProps) {
} = useGetAdminUsageByUsername(selectedAdmin, adminUsageParams, {
query: {
enabled: !shouldUseNodeUsage && selectedAdmin !== 'all' && selectedAdminId == null,
refetchInterval: 1000 * 60 * 5,
refetchInterval: pollingInterval,
},
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Skeleton } from '@/components/ui/skeleton'
import { Badge } from '@/components/ui/badge'
import useDirDetection from '@/hooks/use-dir-detection'
import { useDocumentVisibility } from '@/hooks/use-document-visibility'
import { useTheme } from 'next-themes'
import { type GetUsersSubUpdateChartParams, useGetAdminsSimple, useGetUsersSubUpdateChart, type UserSubscriptionUpdateChartSegment } from '@/service/api'
import { numberWithCommas } from '@/utils/formatByte'
Expand Down Expand Up @@ -136,6 +137,7 @@ function UserSubUpdatePieChart({ username, adminId }: UserSubUpdatePieChartProps
const { t } = useTranslation()
const dir = useDirDetection()
const { resolvedTheme } = useTheme()
const isTabVisible = useDocumentVisibility()
const [selectedAdmin, setSelectedAdmin] = useState(() => (adminId != null ? String(adminId) : 'all'))

useEffect(() => {
Expand Down Expand Up @@ -170,7 +172,7 @@ function UserSubUpdatePieChart({ username, adminId }: UserSubUpdatePieChartProps

const { data, isLoading, error } = useGetUsersSubUpdateChart(params, {
query: {
refetchInterval: 60_000,
refetchInterval: isTabVisible ? 1000 * 60 * 15 : 1000 * 60 * 60,
},
})

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AdminDetails, SystemUsersStats, useGetSystemUsersStats } from '@/service/api'
import { useDocumentVisibility } from '@/hooks/use-document-visibility'
import { UserCog, Users } from 'lucide-react'
import { Suspense, lazy, useEffect, useState, type ComponentProps } from 'react'
import { useTranslation } from 'react-i18next'
Expand Down Expand Up @@ -49,6 +50,7 @@ const AdminStatisticsCard = ({
skipStatsFetch?: boolean
}) => {
const { t } = useTranslation()
const isTabVisible = useDocumentVisibility()
if (!admin) return null

// Send admin_username for specific admin stats, except for 'Total' which shows global stats
Expand All @@ -59,7 +61,7 @@ const AdminStatisticsCard = ({
const { data: adminSystemStats } = useGetSystemUsersStats(systemStatsParams, {
query: {
enabled: shouldFetchStats,
refetchInterval: 5000,
refetchInterval: isTabVisible ? 5000 : 60000,
},
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp
import { DOCUMENTATION } from '@/constants/Project'
import { cn } from '@/lib/utils'
import useDirDetection from '@/hooks/use-dir-detection'
import { useDocumentVisibility } from '@/hooks/use-document-visibility'
import { useGetWorkersHealth } from '@/service/api'
import { ChevronDown, ChevronRight, Clock, HelpCircle, Server, ServerCog } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
Expand Down Expand Up @@ -44,11 +45,12 @@ const dotClassMap: Record<WorkerStatusVariant, string> = {
const WorkersHealthCard = () => {
const { t, i18n } = useTranslation()
const dir = useDirDetection()
const isTabVisible = useDocumentVisibility()
const [pauseRefetch, setPauseRefetch] = useState(false)
const [isCollapsed, setIsCollapsed] = useState(true)
const { data, isLoading, isError } = useGetWorkersHealth({
query: {
refetchInterval: pauseRefetch ? false : 5000,
refetchInterval: pauseRefetch ? false : isTabVisible ? 5000 : 60000,
retry: false,
},
})
Expand Down
4 changes: 3 additions & 1 deletion dashboard/src/features/users/components/users-statistics.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import useDirDetection from '@/hooks/use-dir-detection'
import { useDocumentVisibility } from '@/hooks/use-document-visibility'
import { cn } from '@/lib/utils'
import { type SystemUsersStats, useGetSystemUsersStats } from '@/service/api'
import { UserCheck, Users } from 'lucide-react'
Expand All @@ -10,12 +11,13 @@ import { CountUp } from '@/components/ui/count-up'
const UsersStatistics = () => {
const { t } = useTranslation()
const dir = useDirDetection()
const isTabVisible = useDocumentVisibility()
const [prevData, setPrevData] = useState<SystemUsersStats | null>(null)
const [isIncreased, setIsIncreased] = useState<Record<string, boolean>>({})

const { data } = useGetSystemUsersStats(undefined, {
query: {
refetchInterval: 5000,
refetchInterval: isTabVisible ? 5000 : 60000,
},
})

Expand Down
23 changes: 23 additions & 0 deletions dashboard/src/hooks/use-document-visibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useState, useEffect } from 'react'

/**
* Returns whether the current browser tab is visible to the user.
* Uses the Page Visibility API — returns true if the tab is visible,
* false if minimized, hidden, or in a background tab.
*/
export function useDocumentVisibility(): boolean {
const [isVisible, setIsVisible] = useState<boolean>(() => {
if (typeof document === 'undefined') return true
return document.visibilityState === 'visible'
})

useEffect(() => {
const handleVisibilityChange = () => {
setIsVisible(document.visibilityState === 'visible')
}
document.addEventListener('visibilitychange', handleVisibilityChange)
return () => document.removeEventListener('visibilitychange', handleVisibilityChange)
}, [])

return isVisible
}
2 changes: 1 addition & 1 deletion dashboard/src/hooks/use-version-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export function useVersionCheck(currentVersion: string | null, options: UseVersi
gcTime: CACHE_DURATION * 2,
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchInterval: CACHE_DURATION,
refetchInterval: false,
retry: 1,
})

Expand Down
7 changes: 5 additions & 2 deletions dashboard/src/pages/_dashboard._index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { HostFormSchema, hostFormDefaultValues, type HostFormValues } from '@/fe
import { Separator } from '@/components/ui/separator'
import { useAdmin } from '@/hooks/use-admin'
import { useClipboard } from '@/hooks/use-clipboard'
import { useDocumentVisibility } from '@/hooks/use-document-visibility'
import type { AdminDetails, UserResponse } from '@/service/api'
import { useGetSystemResourceStats, useGetSystemUsersStats } from '@/service/api'
import { getInboundDetails } from '@/service/api'
Expand Down Expand Up @@ -99,6 +100,8 @@ const Dashboard = () => {

const queryClient = useQueryClient()
const { copy } = useClipboard()
const isTabVisible = useDocumentVisibility()
const dashboardPollingInterval = isTabVisible ? 5000 : 60000

/** Match nodes list: delayed refetch so backend can settle after create/update (see nodes-list NodeModal onSuccess). */
const handleNodeModalSuccess = () => {
Expand Down Expand Up @@ -214,13 +217,13 @@ const Dashboard = () => {

const { data: systemResourceStatsData } = useGetSystemResourceStats({
query: {
refetchInterval: 5000,
refetchInterval: dashboardPollingInterval,
},
})

const { data: systemUsersStatsData } = useGetSystemUsersStats(systemUsersStatsParams, {
query: {
refetchInterval: 5000,
refetchInterval: dashboardPollingInterval,
},
})

Expand Down