gitscope
← marzneshin / Diff · 78a3e64 → 78a3e64
Changes 78a3e64..78a3e64
Patch commit
diff --git a/app/db/crud.py b/app/db/crud.py
index ce2876b..7f4d31f 100644
--- a/app/db/crud.py
+++ b/app/db/crud.py
@@ -14,7 +14,6 @@ from app.db.models import (
TLS,
Admin,
Node,
- NodeUsage,
NodeUserUsage,
NotificationReminder,
InboundHost,
@@ -29,7 +28,6 @@ from app.models.node import (
NodeCreate,
NodeModify,
NodeStatus,
- NodeUsageResponse,
)
from app.models.proxy import InboundHost as InboundHostModify
from app.models.service import Service as ServiceModify, ServiceCreate
@@ -759,29 +757,44 @@ def get_nodes(
return query.all()
-def get_nodes_usage(
- db: Session, start: datetime, end: datetime
-) -> List[NodeUsageResponse]:
- usages = dict()
+def get_node_usage(
+ db: Session, start: datetime, end: datetime, node: Node
+) -> TrafficUsageSeries:
+ usages = defaultdict(int)
- usages[0] = NodeUsageResponse( # Main Core
- node_id=None, node_name="Master", uplink=0, downlink=0
+ query = (
+ db.query(
+ NodeUserUsage.created_at, func.sum(NodeUserUsage.used_traffic)
+ )
+ .group_by(NodeUserUsage.created_at)
+ .filter(
+ and_(
+ NodeUserUsage.node_id == node.id,
+ NodeUserUsage.created_at >= start,
+ NodeUserUsage.created_at <= end,
+ )
+ )
)
- for node in db.query(Node).all():
- usages[node.id] = NodeUsageResponse(
- node_id=node.id, node_name=node.name, uplink=0, downlink=0
+
+ for created_at, used_traffic in query.all():
+ usages[created_at.replace(tzinfo=timezone.utc).timestamp()] += int(
+ used_traffic
)
- cond = and_(NodeUsage.created_at >= start, NodeUsage.created_at <= end)
+ result = TrafficUsageSeries(usages=[], total=0)
+ current = start.astimezone(timezone.utc).replace(
+ minute=0, second=0, microsecond=0
+ )
- for v in db.query(NodeUsage).filter(cond):
- try:
- usages[v.node_id or 0].uplink += v.uplink
- usages[v.node_id or 0].downlink += v.downlink
- except KeyError:
- pass
+ while current <= end.replace(tzinfo=timezone.utc):
+ usage = usages.get(current.timestamp()) or 0
+ result.usages.append(
+ (int(current.timestamp()), usage)
+ )
+ result.total += usage
+ current += timedelta(hours=1)
- return list(usages.values())
+ return result
def create_node(db: Session, node: NodeCreate):
diff --git a/app/routes/node.py b/app/routes/node.py
index ea1f9a9..4c4720d 100644
--- a/app/routes/node.py
+++ b/app/routes/node.py
@@ -24,10 +24,10 @@ from app.models.node import (
NodeResponse,
NodeSettings,
NodeStatus,
- NodesUsageResponse,
BackendConfig,
BackendStats,
)
+from app.models.system import TrafficUsageSeries
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/nodes", tags=["Node"])
@@ -75,21 +75,6 @@ def get_node_settings(db: DBDep, admin: SudoAdminDep):
return NodeSettings(certificate=tls.certificate)
-@router.get("/usage", response_model=NodesUsageResponse)
-def get_usage(
- db: DBDep,
- admin: SudoAdminDep,
- start_date: StartDateDep,
- end_date: EndDateDep,
-):
- """
- Get nodes usage
- """
- usages = crud.get_nodes_usage(db, start_date, end_date)
-
- return {"usages": usages}
-
-
@router.get("/{node_id}", response_model=NodeResponse)
def get_node(node_id: int, db: DBDep, admin: SudoAdminDep):
db_node = crud.get_node_by_id(db, node_id)
@@ -172,6 +157,24 @@ async def reconnect_node(node_id: int, db: DBDep, admin: SudoAdminDep):
return {}
+@router.get("/{node_id}/usage", response_model=TrafficUsageSeries)
+def get_usage(
+ node_id: int,
+ db: DBDep,
+ admin: SudoAdminDep,
+ start_date: StartDateDep,
+ end_date: EndDateDep,
+):
+ """
+ Get nodes usage
+ """
+ node = crud.get_node_by_id(db, node_id)
+ if not node:
+ raise HTTPException(status_code=404, detail="Node not found")
+
+ return crud.get_node_usage(db, start_date, end_date, node)
+
+
@router.get("/{node_id}/{backend}/stats", response_model=BackendStats)
async def get_backend_stats(
node_id: int, backend: str, db: DBDep, admin: SudoAdminDep
diff --git a/dashboard/public/locales/en.json b/dashboard/public/locales/en.json
index b365d3b..5695d75 100644
--- a/dashboard/public/locales/en.json
+++ b/dashboard/public/locales/en.json
@@ -1,5 +1,6 @@
{
"name": "Name",
+ "info": "Info",
"status": "Status",
"address": "Address",
"users": "Users",
@@ -130,6 +131,10 @@
}
},
"nodes": {
+ "nodes-usage-widget": {
+ "title": "Node Traffic Usage",
+ "desc": "Total traffic usage on this node"
+ },
"settings": {
"no-backend-alert": {
"title": "No backend is configured for this node",
diff --git a/dashboard/src/common/stats-charts/hooks/index.ts b/dashboard/src/common/stats-charts/hooks/index.ts
index 2aac369..10cc07d 100644
--- a/dashboard/src/common/stats-charts/hooks/index.ts
+++ b/dashboard/src/common/stats-charts/hooks/index.ts
@@ -1 +1,2 @@
export * from "./use-from-now-interval"
+export * from "./use-transform-date-usage-data"
diff --git a/dashboard/src/features/total-traffic-widget/hooks/use-transform-data.ts b/dashboard/src/common/stats-charts/hooks/use-transform-date-usage-data.ts
similarity index 50%
rename from dashboard/src/features/total-traffic-widget/hooks/use-transform-data.ts
rename to dashboard/src/common/stats-charts/hooks/use-transform-date-usage-data.ts
index 59c4afb..f70a6b9 100644
--- a/dashboard/src/features/total-traffic-widget/hooks/use-transform-data.ts
+++ b/dashboard/src/common/stats-charts/hooks/use-transform-date-usage-data.ts
@@ -1,13 +1,6 @@
-import { UsageMetric } from "../api";
+import { UsageMetric, ChartData } from "./../types";
-export type ChartDataEntry = {
- datetime: string;
- [key: string]: number | string;
-};
-
-export type ChartData = ChartDataEntry[];
-
-export function useTransformData(usages: Array<UsageMetric>): ChartData {
+export function useTransformDateUsageData(usages: Array<UsageMetric>): ChartData {
return usages.map(usageData => {
const [timestamp, usage] = usageData;
const date = new Date(timestamp * 1000);
diff --git a/dashboard/src/common/stats-charts/types.ts b/dashboard/src/common/stats-charts/types.ts
index 481900c..55253ed 100644
--- a/dashboard/src/common/stats-charts/types.ts
+++ b/dashboard/src/common/stats-charts/types.ts
@@ -1,2 +1,10 @@
+export type UsageMetric = number[];
+
+export type ChartDataEntry = {
+ datetime: string;
+ [key: string]: number | string;
+};
+
+export type ChartData = ChartDataEntry[];
export type ChartDateInterval = '90d' | '30d' | '7d' | '1d';
diff --git a/dashboard/src/features/inbounds/types/index.ts b/dashboard/src/features/inbounds/types/index.ts
index 919a537..ae7ab40 100644
--- a/dashboard/src/features/inbounds/types/index.ts
+++ b/dashboard/src/features/inbounds/types/index.ts
@@ -1,4 +1,4 @@
-import { NodeType } from "@marzneshin/features/nodes";
+import { NodeType } from "@marzneshin/modules/nodes";
export type ProtocolType = 'vmess' | 'vless' | 'trojan' | 'shadowsocks';
diff --git a/dashboard/src/features/nodes/dialogs/settings/index.tsx b/dashboard/src/features/nodes/dialogs/settings/index.tsx
deleted file mode 100644
index 16424b6..0000000
--- a/dashboard/src/features/nodes/dialogs/settings/index.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import {
- type SettingsDialogProps,
- SettingsDialog,
- Tabs,
- TabsContent,
- TabsList,
- TabsTrigger,
- AlertCard,
-} from "@marzneshin/components";
-import type { FC } from "react";
-import { useTranslation } from "react-i18next";
-import type { NodeBackendType, NodeType } from "../..";
-import { NodesDetailTable } from "../../tables/detail-table";
-import { NodeBackendSetting } from "./node-backend-setting";
-
-interface NodesSettingsDialogProps extends SettingsDialogProps {
- entity: NodeType;
- onClose: () => void;
-}
-
-export const NodesSettingsDialog: FC<NodesSettingsDialogProps> = ({
- onOpenChange,
- open,
- entity,
- onClose,
-}) => {
- const { t } = useTranslation();
-
- return (
- <SettingsDialog open={open} onClose={onClose} onOpenChange={onOpenChange}>
- <div className="my-4">
- <h1 className="font-medium font-header">
- {t("page.nodes.settings.detail")}
- </h1>
- <NodesDetailTable node={entity} />
- </div>
- {entity.backends.length === 0 ? (
- <AlertCard
- variant="warning"
- desc={t('page.nodes.settings.no-backend-alert.desc')}
- title={t('page.nodes.settings.no-backend-alert.title')}
- />
- ) : (
- <Tabs
- className="my-3 w-full h-full"
- defaultValue={entity.backends[0].name}
- >
- <TabsList className="w-full">
- {...entity.backends.map((backend: NodeBackendType) => (
- <TabsTrigger
- className="capitalize w-full"
- value={backend.name}
- key={backend.name}
- >
- {backend.name}
- </TabsTrigger>
- ))}
- </TabsList>
- {...entity.backends.map((backend: NodeBackendType) => (
- <TabsContent
- className="w-full"
- value={backend.name}
- key={backend.name}
- >
- <NodeBackendSetting node={entity} backend={backend.name} />
- </TabsContent>
- ))}
- </Tabs>
- )}
- </SettingsDialog>
- );
-};
diff --git a/dashboard/src/features/settings/services/certificate.query.ts b/dashboard/src/features/settings/services/certificate.query.ts
index ffe462b..69d3e1f 100644
--- a/dashboard/src/features/settings/services/certificate.query.ts
+++ b/dashboard/src/features/settings/services/certificate.query.ts
@@ -1,6 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { fetch } from "@marzneshin/utils";
-import { NodesQueryFetchKey } from "@marzneshin/features/nodes";
+import { NodesQueryFetchKey } from "@marzneshin/modules/nodes";
export async function fetchCertificate(): Promise<string> {
return fetch('/nodes/settings').then((settings) => {
diff --git a/dashboard/src/features/subscription-settings/services/subscription-settings.mutate.ts b/dashboard/src/features/subscription-settings/services/subscription-settings.mutate.ts
index cd21848..9f0bfec 100644
--- a/dashboard/src/features/subscription-settings/services/subscription-settings.mutate.ts
+++ b/dashboard/src/features/subscription-settings/services/subscription-settings.mutate.ts
@@ -1,4 +1,4 @@
-import { NodeType } from "@marzneshin/features/nodes";
+import { NodeType } from "@marzneshin/modules/nodes";
import { useMutation } from "@tanstack/react-query";
import { fetch, queryClient } from "@marzneshin/utils";
import { toast } from "sonner";
diff --git a/dashboard/src/features/total-traffic-widget/hooks/index.ts b/dashboard/src/features/total-traffic-widget/hooks/index.ts
deleted file mode 100644
index d8f6e9a..0000000
--- a/dashboard/src/features/total-traffic-widget/hooks/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "./use-transform-data"
diff --git a/dashboard/src/features/total-traffic-widget/index.tsx b/dashboard/src/features/total-traffic-widget/index.tsx
index df4e297..c223382 100644
--- a/dashboard/src/features/total-traffic-widget/index.tsx
+++ b/dashboard/src/features/total-traffic-widget/index.tsx
@@ -9,13 +9,13 @@ import {
ChartConfig,
} from "@marzneshin/components";
import { XAxis, BarChart, YAxis, CartesianGrid, Bar } from "recharts"
-import { useTransformData } from "./hooks";
import { format as formatByte } from '@chbphone55/pretty-bytes';
import { useTotalTrafficQuery } from "./api";
import { UsageGraphSkeleton } from "./components";
import {
ChartDateInterval,
SelectDateView,
+ useTransformDateUsageData,
dateXAxisTicks,
useFromNowInterval
} from "@marzneshin/common/stats-charts";
@@ -32,7 +32,7 @@ export const TotalTrafficsWidget: FC = () => {
const [timeRange, setTimeRange] = useState("1d")
const { start, end } = useFromNowInterval(timeRange as ChartDateInterval);
const { data, isPending } = useTotalTrafficQuery({ start, end })
- const chartData = useTransformData(data.usages);
+ const chartData = useTransformDateUsageData(data.usages);
const [totalAmount, totalMetric] = formatByte(data.total);
return (
diff --git a/dashboard/src/features/nodes/dialogs/delete-confirmation/index.tsx b/dashboard/src/modules/nodes/dialogs/delete-confirmation/index.tsx
similarity index 97%
rename from dashboard/src/features/nodes/dialogs/delete-confirmation/index.tsx
rename to dashboard/src/modules/nodes/dialogs/delete-confirmation/index.tsx
index 2e76558..53f8a68 100644
--- a/dashboard/src/features/nodes/dialogs/delete-confirmation/index.tsx
+++ b/dashboard/src/modules/nodes/dialogs/delete-confirmation/index.tsx
@@ -1,7 +1,7 @@
import { DeleteConfirmation } from '@marzneshin/components'
import { FC, useEffect } from 'react'
-import { NodeType, useNodesDeletionMutation } from '@marzneshin/features/nodes'
+import { NodeType, useNodesDeletionMutation } from '@marzneshin/modules/nodes'
interface NodesDeleteConfirmationDialogProps {
onOpenChange: (state: boolean) => void
diff --git a/dashboard/src/features/nodes/dialogs/index.ts b/dashboard/src/modules/nodes/dialogs/index.ts
similarity index 100%
rename from dashboard/src/features/nodes/dialogs/index.ts
rename to dashboard/src/modules/nodes/dialogs/index.ts
diff --git a/dashboard/src/features/nodes/dialogs/mutation/index.tsx b/dashboard/src/modules/nodes/dialogs/mutation/index.tsx
similarity index 100%
rename from dashboard/src/features/nodes/dialogs/mutation/index.tsx
rename to dashboard/src/modules/nodes/dialogs/mutation/index.tsx
diff --git a/dashboard/src/modules/nodes/dialogs/settings/index.tsx b/dashboard/src/modules/nodes/dialogs/settings/index.tsx
new file mode 100644
index 0000000..babc502
--- /dev/null
+++ b/dashboard/src/modules/nodes/dialogs/settings/index.tsx
@@ -0,0 +1,87 @@
+import {
+ type SettingsDialogProps,
+ SettingsDialog,
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger,
+ AlertCard,
+} from "@marzneshin/components";
+import type { FC } from "react";
+import { useTranslation } from "react-i18next";
+import {
+ NodeBackendType,
+ NodeType,
+ NodesDetailTable,
+ NodesUsageWidget
+} from "@marzneshin/modules/nodes";
+import { NodeBackendSetting } from "./node-backend-setting";
+
+interface NodesSettingsDialogProps extends SettingsDialogProps {
+ entity: NodeType;
+ onClose: () => void;
+}
+
+export const NodesSettingsDialog: FC<NodesSettingsDialogProps> = ({
+ onOpenChange,
+ open,
+ entity,
+ onClose,
+}) => {
+ const { t } = useTranslation();
+
+ return (
+ <SettingsDialog open={open} onClose={onClose} onOpenChange={onOpenChange}>
+ <Tabs defaultValue="config">
+ <TabsList className="w-full">
+ <TabsTrigger className="w-full" value="config">{t("config")}</TabsTrigger>
+ <TabsTrigger className="w-full" value="info">{t("info")}</TabsTrigger>
+ </TabsList>
+ <TabsContent value="config">
+ {entity.backends.length === 0 ? (
+ <AlertCard
+ variant="warning"
+ desc={t('page.nodes.settings.no-backend-alert.desc')}
+ title={t('page.nodes.settings.no-backend-alert.title')}
+ />
+ ) : (
+ <Tabs
+ className="my-3 w-full h-full"
+ defaultValue={entity.backends[0].name}
+ >
+ <TabsList className="w-full">
+ {...entity.backends.map((backend: NodeBackendType) => (
+ <TabsTrigger
+ className="capitalize w-full"
+ value={backend.name}
+ key={backend.name}
+ >
+ {backend.name}
+ </TabsTrigger>
+ ))}
+ </TabsList>
+ {...entity.backends.map((backend: NodeBackendType) => (
+ <TabsContent
+ className="w-full"
+ value={backend.name}
+ key={backend.name}
+ >
+ <NodeBackendSetting node={entity} backend={backend.name} />
+ </TabsContent>
+ ))}
+ </Tabs>
+ )}
+ </TabsContent>
+ <TabsContent value="info">
+ <div className="my-4">
+ <h1 className="font-medium font-header">
+ {t("page.nodes.settings.detail")}
+ </h1>
+ <NodesDetailTable node={entity} />
+ </div>
+ <NodesUsageWidget node={entity} />
+ </TabsContent>
+ </Tabs>
+ </SettingsDialog>
+ );
+};
diff --git a/dashboard/src/features/nodes/dialogs/settings/node-backend-setting.tsx b/dashboard/src/modules/nodes/dialogs/settings/node-backend-setting.tsx
similarity index 100%
rename from dashboard/src/features/nodes/dialogs/settings/node-backend-setting.tsx
rename to dashboard/src/modules/nodes/dialogs/settings/node-backend-setting.tsx
diff --git a/dashboard/src/features/nodes/dialogs/settings/node-config-editor.tsx b/dashboard/src/modules/nodes/dialogs/settings/node-config-editor.tsx
similarity index 100%
rename from dashboard/src/features/nodes/dialogs/settings/node-config-editor.tsx
rename to dashboard/src/modules/nodes/dialogs/settings/node-config-editor.tsx
diff --git a/dashboard/src/features/nodes/dialogs/settings/use-nodes-settings.ts b/dashboard/src/modules/nodes/dialogs/settings/use-nodes-settings.ts
similarity index 100%
rename from dashboard/src/features/nodes/dialogs/settings/use-nodes-settings.ts
rename to dashboard/src/modules/nodes/dialogs/settings/use-nodes-settings.ts
diff --git a/dashboard/src/features/nodes/index.ts b/dashboard/src/modules/nodes/index.ts
similarity index 78%
rename from dashboard/src/features/nodes/index.ts
rename to dashboard/src/modules/nodes/index.ts
index bd7acfa..6955b0d 100644
--- a/dashboard/src/features/nodes/index.ts
+++ b/dashboard/src/modules/nodes/index.ts
@@ -1,5 +1,6 @@
export * from './types';
export * from './tables';
export * from './services';
+export * from './nodes-usage-widget';
export * from './dialogs';
export * from './router-context';
diff --git a/dashboard/src/features/nodes/log/container.tsx b/dashboard/src/modules/nodes/log/container.tsx
similarity index 100%
rename from dashboard/src/features/nodes/log/container.tsx
rename to dashboard/src/modules/nodes/log/container.tsx
diff --git a/dashboard/src/features/nodes/log/index.ts b/dashboard/src/modules/nodes/log/index.ts
similarity index 100%
rename from dashboard/src/features/nodes/log/index.ts
rename to dashboard/src/modules/nodes/log/index.ts
diff --git a/dashboard/src/modules/nodes/nodes-usage-widget/index.tsx b/dashboard/src/modules/nodes/nodes-usage-widget/index.tsx
new file mode 100644
index 0000000..f513983
--- /dev/null
+++ b/dashboard/src/modules/nodes/nodes-usage-widget/index.tsx
@@ -0,0 +1,117 @@
+import { useState, FC } from 'react';
+import { useTranslation } from 'react-i18next';
+import {
+ SectionWidget,
+ ChartContainer,
+ Awaiting,
+ ChartTooltip,
+ ChartTooltipContent,
+ ChartConfig,
+} from "@marzneshin/components";
+import { XAxis, BarChart, YAxis, CartesianGrid, Bar } from "recharts"
+import { format as formatByte } from '@chbphone55/pretty-bytes';
+import { useNodesUsageQuery, NodeType } from "@marzneshin/modules/nodes";
+import {
+ useTransformDateUsageData,
+ ChartDateInterval,
+ SelectDateView,
+ dateXAxisTicks,
+ useFromNowInterval
+} from "@marzneshin/common/stats-charts";
+
+const chartConfig = {
+ traffic: {
+ label: "Traffic",
+ color: "hsl(var(--chart-1))",
+ },
+} satisfies ChartConfig
+
+export const NodesUsageWidget: FC<{ node: NodeType }> = ({ node }) => {
+ const { t } = useTranslation();
+ const [timeRange, setTimeRange] = useState("1d")
+ const { start, end } = useFromNowInterval(timeRange as ChartDateInterval);
+ const { data } = useNodesUsageQuery({ nodeId: node.id, start, end })
+ const chartData = useTransformDateUsageData(data.usages);
+ const [totalAmount, totalMetric] = formatByte(data.total);
+
+ return (
+ <Awaiting
+ Component={
+ <SectionWidget
+ title={
+ <div className="hstack justify-between w-full">
+ {t("page.nodes.nodes-usage-widget.title")}
+ </div>
+ }
+ description={t("page.nodes.nodes-usage-widget.desc")}
+ options={
+ <div className="vstack justify-end w-full">
+ <span className="text-lg leading-none sm:text-2xl w-full">
+ {totalAmount} {totalMetric}
+ </span>
+ <span className="text-sm flex justify-end text-muted-foreground w-full">
+ Total
+ </span>
+ </div>
+ }
+ footer={
+ <SelectDateView timeRange={timeRange} setTimeRange={setTimeRange} />
+ }
+ >
+ <ChartContainer
+ config={chartConfig}
+ className="aspect-auto h-[320px] w-full"
+ >
+ <BarChart
+ accessibilityLayer
+ data={chartData}
+ margin={{
+ left: 12,
+ top: 13,
+ right: 12,
+ }}
+ >
+ <CartesianGrid vertical={false} />
+ <YAxis
+ tickLine={false}
+ axisLine={false}
+ tickMargin={8}
+ tickFormatter={(value) => {
+ const [amount, metric] = formatByte(value)
+ return `${amount} ${metric}`
+ }}
+ />
+ <XAxis
+ dataKey="datetime"
+ tickLine={false}
+ axisLine={false}
+ tickMargin={8}
+ tickFormatter={(value) => dateXAxisTicks(value, timeRange as ChartDateInterval)}
+ />
+ <ChartTooltip
+ content={
+ <ChartTooltipContent
+ indicator='line'
+ labelFormatter={(value) => {
+ return new Date(value).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ hour: "numeric"
+ })
+ }}
+ valueFormatter={(value) => {
+ const [amount, metric] = formatByte(value as number)
+ return `${amount} ${metric}`
+ }}
+ />
+ }
+ />
+ <Bar dataKey="traffic" fill={`var(--color-traffic)`} />
+ </BarChart>
+ </ChartContainer>
+ </SectionWidget>
+ }
+ />
+ );
+};
diff --git a/dashboard/src/modules/nodes/nodes-usage-widget/types.ts b/dashboard/src/modules/nodes/nodes-usage-widget/types.ts
new file mode 100644
index 0000000..a601179
--- /dev/null
+++ b/dashboard/src/modules/nodes/nodes-usage-widget/types.ts
@@ -0,0 +1,10 @@
+import { UserType } from "@marzneshin/modules/users";
+
+export interface NodesUsage {
+ datetime: Date;
+ nodes: Record<string, number>;
+}
+
+export interface UserNodesUsageWidgetProps {
+ user: UserType;
+}
diff --git a/dashboard/src/features/nodes/router-context/index.ts b/dashboard/src/modules/nodes/router-context/index.ts
similarity index 84%
rename from dashboard/src/features/nodes/router-context/index.ts
rename to dashboard/src/modules/nodes/router-context/index.ts
index d5a3369..d7cce73 100644
--- a/dashboard/src/features/nodes/router-context/index.ts
+++ b/dashboard/src/modules/nodes/router-context/index.ts
@@ -1,5 +1,5 @@
import { createContext, useContext } from "react";
-import { NodeType } from "@marzneshin/features/nodes";
+import { NodeType } from "@marzneshin/modules/nodes";
interface RouterNodeContextProps {
node: NodeType;
diff --git a/dashboard/src/features/nodes/services/backend-stats.query.ts b/dashboard/src/modules/nodes/services/backend-stats.query.ts
similarity index 93%
rename from dashboard/src/features/nodes/services/backend-stats.query.ts
rename to dashboard/src/modules/nodes/services/backend-stats.query.ts
index bcd5ec8..d4e55d6 100644
--- a/dashboard/src/features/nodes/services/backend-stats.query.ts
+++ b/dashboard/src/modules/nodes/services/backend-stats.query.ts
@@ -1,4 +1,4 @@
-import type { NodeType } from "@marzneshin/features/nodes";
+import type { NodeType } from "@marzneshin/modules/nodes";
import { useQuery } from "@tanstack/react-query";
import { fetch } from "@marzneshin/utils";
diff --git a/dashboard/src/features/nodes/services/create-node.mutate.ts b/dashboard/src/modules/nodes/services/create-node.mutate.ts
similarity index 93%
rename from dashboard/src/features/nodes/services/create-node.mutate.ts
rename to dashboard/src/modules/nodes/services/create-node.mutate.ts
index 69095dd..f123dd2 100644
--- a/dashboard/src/features/nodes/services/create-node.mutate.ts
+++ b/dashboard/src/modules/nodes/services/create-node.mutate.ts
@@ -1,4 +1,4 @@
-import { NodeType, NodesQueryFetchKey } from "@marzneshin/features/nodes";
+import { NodeType, NodesQueryFetchKey } from "@marzneshin/modules/nodes";
import { useMutation } from "@tanstack/react-query";
import { fetch, queryClient } from "@marzneshin/utils";
import { toast } from "sonner";
diff --git a/dashboard/src/features/nodes/services/delete-node.mutate.ts b/dashboard/src/modules/nodes/services/delete-node.mutate.ts
similarity index 93%
rename from dashboard/src/features/nodes/services/delete-node.mutate.ts
rename to dashboard/src/modules/nodes/services/delete-node.mutate.ts
index de2a107..714b9cd 100644
--- a/dashboard/src/features/nodes/services/delete-node.mutate.ts
+++ b/dashboard/src/modules/nodes/services/delete-node.mutate.ts
@@ -1,5 +1,5 @@
-import { NodeType, NodesQueryFetchKey } from "@marzneshin/features/nodes";
+import { NodeType, NodesQueryFetchKey } from "@marzneshin/modules/nodes";
import { useMutation } from "@tanstack/react-query";
import { fetch, queryClient } from "@marzneshin/utils";
import { toast } from "sonner";
diff --git a/dashboard/src/features/nodes/services/index.ts b/dashboard/src/modules/nodes/services/index.ts
similarity index 89%
rename from dashboard/src/features/nodes/services/index.ts
rename to dashboard/src/modules/nodes/services/index.ts
index ad40cfd..a57298e 100644
--- a/dashboard/src/features/nodes/services/index.ts
+++ b/dashboard/src/modules/nodes/services/index.ts
@@ -2,6 +2,7 @@ export * from "./nodes.query";
export * from "./node.query";
export * from "./logs.socket";
export * from "./settings.query";
+export * from "./nodes-usage.query";
export * from "./backend-stats.query";
export * from "./settings.mutate";
export * from "./update-node.mutate";
diff --git a/dashboard/src/features/nodes/services/logs.socket.ts b/dashboard/src/modules/nodes/services/logs.socket.ts
similarity index 97%
rename from dashboard/src/features/nodes/services/logs.socket.ts
rename to dashboard/src/modules/nodes/services/logs.socket.ts
index 6412995..50a3a06 100644
--- a/dashboard/src/features/nodes/services/logs.socket.ts
+++ b/dashboard/src/modules/nodes/services/logs.socket.ts
@@ -1,4 +1,4 @@
-import { NodeType } from "@marzneshin/features/nodes";
+import { NodeType } from "@marzneshin/modules/nodes";
import useWebSocket, { ReadyState } from "react-use-websocket";
import { useAuth } from "@marzneshin/features/auth";
import { joinPaths } from "@tanstack/react-router";
diff --git a/dashboard/src/features/nodes/services/node.query.ts b/dashboard/src/modules/nodes/services/node.query.ts
similarity index 100%
rename from dashboard/src/features/nodes/services/node.query.ts
rename to dashboard/src/modules/nodes/services/node.query.ts
diff --git a/dashboard/src/modules/nodes/services/nodes-usage.query.ts b/dashboard/src/modules/nodes/services/nodes-usage.query.ts
new file mode 100644
index 0000000..280feaf
--- /dev/null
+++ b/dashboard/src/modules/nodes/services/nodes-usage.query.ts
@@ -0,0 +1,41 @@
+import { useQuery } from "@tanstack/react-query";
+import { fetch } from "@marzneshin/utils";
+
+export type UsageMetric = number[];
+
+export interface NodesUsageResponse {
+ usages: Array<UsageMetric>;
+ total: number;
+}
+
+export interface NodesUsageQueryOptions {
+ nodeId: number;
+ start: string;
+ end: string;
+}
+
+export type NodesUsageQueryKey = [string, number, string, { start?: string, end?: string }]
+
+export const NodesUsageDefault = {
+ total: 0,
+ usages: []
+}
+
+export async function fetchNodesUsage({ queryKey }: { queryKey: NodesUsageQueryKey }): Promise<NodesUsageResponse> {
+ return await fetch(`/nodes/${queryKey[1]}/usage`, {
+ query: {
+ start: queryKey[3].start,
+ end: queryKey[3].end
+ }
+ }).then((result) => {
+ return result;
+ });
+}
+export const useNodesUsageQuery = ({ nodeId, start, end }: NodesUsageQueryOptions) => {
+ return useQuery({
+ queryKey: ["nodes", nodeId, "usage", { start, end }],
+ queryFn: fetchNodesUsage,
+ refetchInterval: 1000 * 60 * 5, // 60min refresh
+ initialData: NodesUsageDefault
+ })
+}
diff --git a/dashboard/src/features/nodes/services/nodes.query.ts b/dashboard/src/modules/nodes/services/nodes.query.ts
similarity index 95%
rename from dashboard/src/features/nodes/services/nodes.query.ts
rename to dashboard/src/modules/nodes/services/nodes.query.ts
index ae6a8c7..8759c21 100644
--- a/dashboard/src/features/nodes/services/nodes.query.ts
+++ b/dashboard/src/modules/nodes/services/nodes.query.ts
@@ -1,4 +1,4 @@
-import type { NodeType } from "@marzneshin/features/nodes";
+import type { NodeType } from "@marzneshin/modules/nodes";
import { useQuery } from "@tanstack/react-query";
import { fetch } from "@marzneshin/utils";
import type {
diff --git a/dashboard/src/features/nodes/services/settings.mutate.ts b/dashboard/src/modules/nodes/services/settings.mutate.ts
similarity index 96%
rename from dashboard/src/features/nodes/services/settings.mutate.ts
rename to dashboard/src/modules/nodes/services/settings.mutate.ts
index db60da3..afd136a 100644
--- a/dashboard/src/features/nodes/services/settings.mutate.ts
+++ b/dashboard/src/modules/nodes/services/settings.mutate.ts
@@ -1,4 +1,4 @@
-import type { NodeType } from "@marzneshin/features/nodes";
+import type { NodeType } from "@marzneshin/modules/nodes";
import { useMutation } from "@tanstack/react-query";
import { fetch, queryClient } from "@marzneshin/utils";
import { toast } from "sonner";
diff --git a/dashboard/src/features/nodes/services/settings.query.ts b/dashboard/src/modules/nodes/services/settings.query.ts
similarity index 94%
rename from dashboard/src/features/nodes/services/settings.query.ts
rename to dashboard/src/modules/nodes/services/settings.query.ts
index b1b8f70..b456362 100644
--- a/dashboard/src/features/nodes/services/settings.query.ts
+++ b/dashboard/src/modules/nodes/services/settings.query.ts
@@ -1,4 +1,4 @@
-import type { NodeType } from "@marzneshin/features/nodes";
+import type { NodeType } from "@marzneshin/modules/nodes";
import { useQuery } from "@tanstack/react-query";
import { fetch } from "@marzneshin/utils";
diff --git a/dashboard/src/features/nodes/services/update-node.mutate.ts b/dashboard/src/modules/nodes/services/update-node.mutate.ts
similarity index 95%
rename from dashboard/src/features/nodes/services/update-node.mutate.ts
rename to dashboard/src/modules/nodes/services/update-node.mutate.ts
index fc3b313..772022b 100644
--- a/dashboard/src/features/nodes/services/update-node.mutate.ts
+++ b/dashboard/src/modules/nodes/services/update-node.mutate.ts
@@ -1,4 +1,4 @@
-import { NodeType } from "@marzneshin/features/nodes";
+import { NodeType } from "@marzneshin/modules/nodes";
import { useMutation } from "@tanstack/react-query";
import { fetch, queryClient } from "@marzneshin/utils";
import { toast } from "sonner";
diff --git a/dashboard/src/features/nodes/tables/columns.tsx b/dashboard/src/modules/nodes/tables/columns.tsx
similarity index 98%
rename from dashboard/src/features/nodes/tables/columns.tsx
rename to dashboard/src/modules/nodes/tables/columns.tsx
index bd5fa9f..4391502 100644
--- a/dashboard/src/features/nodes/tables/columns.tsx
+++ b/dashboard/src/modules/nodes/tables/columns.tsx
@@ -1,5 +1,5 @@
import { ColumnDef } from "@tanstack/react-table"
-import { NodesStatusBadge, NodeType, NodesStatus } from "@marzneshin/features/nodes"
+import { NodesStatusBadge, NodeType, NodesStatus } from "@marzneshin/modules/nodes"
import {
DataTableActionsCell,
DataTableColumnHeader
diff --git a/dashboard/src/features/nodes/tables/detail-table/index.tsx b/dashboard/src/modules/nodes/tables/detail-table/index.tsx
similarity index 100%
rename from dashboard/src/features/nodes/tables/detail-table/index.tsx
rename to dashboard/src/modules/nodes/tables/detail-table/index.tsx
diff --git a/dashboard/src/features/nodes/tables/index.ts b/dashboard/src/modules/nodes/tables/index.ts
similarity index 73%
rename from dashboard/src/features/nodes/tables/index.ts
rename to dashboard/src/modules/nodes/tables/index.ts
index 1ba1b35..5cda0a6 100644
--- a/dashboard/src/features/nodes/tables/index.ts
+++ b/dashboard/src/modules/nodes/tables/index.ts
@@ -1,3 +1,4 @@
export * from "./columns"
+export * from "./detail-table"
export * from "./nodes-status-badge"
export * from "./table"
diff --git a/dashboard/src/features/nodes/tables/nodes-status-badge.tsx b/dashboard/src/modules/nodes/tables/nodes-status-badge.tsx
similarity index 100%
rename from dashboard/src/features/nodes/tables/nodes-status-badge.tsx
rename to dashboard/src/modules/nodes/tables/nodes-status-badge.tsx
diff --git a/dashboard/src/features/nodes/tables/table.tsx b/dashboard/src/modules/nodes/tables/table.tsx
similarity index 96%
rename from dashboard/src/features/nodes/tables/table.tsx
rename to dashboard/src/modules/nodes/tables/table.tsx
index 831a8f8..278daed 100644
--- a/dashboard/src/features/nodes/tables/table.tsx
+++ b/dashboard/src/modules/nodes/tables/table.tsx
@@ -3,7 +3,7 @@ import {
columns as columnsFn,
fetchNodes,
NodeType
-} from '@marzneshin/features/nodes';
+} from '@marzneshin/modules/nodes';
import { EntityTable } from "@marzneshin/features/entity-table";
import { useNavigate } from "@tanstack/react-router";
diff --git a/dashboard/src/features/nodes/types/index.ts b/dashboard/src/modules/nodes/types/index.ts
similarity index 100%
rename from dashboard/src/features/nodes/types/index.ts
rename to dashboard/src/modules/nodes/types/index.ts
diff --git a/dashboard/src/routes/_dashboard/nodes.lazy.tsx b/dashboard/src/routes/_dashboard/nodes.lazy.tsx
index 154ed37..a8eb3cb 100644
--- a/dashboard/src/routes/_dashboard/nodes.lazy.tsx
+++ b/dashboard/src/routes/_dashboard/nodes.lazy.tsx
@@ -3,7 +3,7 @@ import {
Page,
Loading,
} from '@marzneshin/components';
-import { NodesTable } from '@marzneshin/features/nodes';
+import { NodesTable } from '@marzneshin/modules/nodes';
import { Link, createFileRoute, Outlet } from '@tanstack/react-router'
import { type FC, Suspense } from 'react';
import { useTranslation } from 'react-i18next';
diff --git a/dashboard/src/routes/_dashboard/nodes/$nodeId.tsx b/dashboard/src/routes/_dashboard/nodes/$nodeId.tsx
index 2daaec4..eb60a20 100644
--- a/dashboard/src/routes/_dashboard/nodes/$nodeId.tsx
+++ b/dashboard/src/routes/_dashboard/nodes/$nodeId.tsx
@@ -7,7 +7,7 @@ import {
import {
fetchNode,
RouterNodeContext
-} from "@marzneshin/features/nodes";
+} from "@marzneshin/modules/nodes";
import { Suspense } from "react";
import {
AlertDialog,
diff --git a/dashboard/src/routes/_dashboard/nodes/$nodeId/delete.tsx b/dashboard/src/routes/_dashboard/nodes/$nodeId/delete.tsx
index 586cee4..ef2046c 100644
--- a/dashboard/src/routes/_dashboard/nodes/$nodeId/delete.tsx
+++ b/dashboard/src/routes/_dashboard/nodes/$nodeId/delete.tsx
@@ -5,7 +5,7 @@ import {
import {
NodesDeleteConfirmationDialog,
useRouterNodeContext,
-} from "@marzneshin/features/nodes";
+} from "@marzneshin/modules/nodes";
import { useDialog } from "@marzneshin/hooks";
const NodeDelete = () => {
diff --git a/dashboard/src/routes/_dashboard/nodes/$nodeId/edit.tsx b/dashboard/src/routes/_dashboard/nodes/$nodeId/edit.tsx
index 6f874e2..d52e042 100644
--- a/dashboard/src/routes/_dashboard/nodes/$nodeId/edit.tsx
+++ b/dashboard/src/routes/_dashboard/nodes/$nodeId/edit.tsx
@@ -5,7 +5,7 @@ import {
import {
MutationDialog,
useRouterNodeContext,
-} from "@marzneshin/features/nodes";
+} from "@marzneshin/modules/nodes";
const NodeEdit = () => {
const value = useRouterNodeContext()
diff --git a/dashboard/src/routes/_dashboard/nodes/$nodeId/index.tsx b/dashboard/src/routes/_dashboard/nodes/$nodeId/index.tsx
index a0505af..dc33800 100644
--- a/dashboard/src/routes/_dashboard/nodes/$nodeId/index.tsx
+++ b/dashboard/src/routes/_dashboard/nodes/$nodeId/index.tsx
@@ -5,7 +5,7 @@ import {
import {
useRouterNodeContext,
NodesSettingsDialog,
-} from "@marzneshin/features/nodes";
+} from "@marzneshin/modules/nodes";
import { useDialog } from "@marzneshin/hooks";
const NodeOpen = () => {
diff --git a/dashboard/src/routes/_dashboard/nodes/create.tsx b/dashboard/src/routes/_dashboard/nodes/create.tsx
index efc22de..9eb5636 100644
--- a/dashboard/src/routes/_dashboard/nodes/create.tsx
+++ b/dashboard/src/routes/_dashboard/nodes/create.tsx
@@ -1,5 +1,5 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
-import { MutationDialog } from "@marzneshin/features/nodes";
+import { MutationDialog } from "@marzneshin/modules/nodes";
const NodeCreate = () => {
const navigate = useNavigate({ from: "/nodes/create" });