diff --git a/src/shared/api/contracts/index.ts b/src/shared/api/contracts/index.ts new file mode 100644 index 00000000..fe029d12 --- /dev/null +++ b/src/shared/api/contracts/index.ts @@ -0,0 +1 @@ +export * from './veil-node.contract' diff --git a/src/shared/api/contracts/veil-node.contract.ts b/src/shared/api/contracts/veil-node.contract.ts new file mode 100644 index 00000000..63b02b4c --- /dev/null +++ b/src/shared/api/contracts/veil-node.contract.ts @@ -0,0 +1,36 @@ +// Inline contract extension that adds the `core: 'XRAY' | 'VEIL'` +// field to the Create/Update node request schemas. +// +// Why this lives here: +// @remnawave/backend-contract@2.7.2 (the version pinned by this +// branch) ships the legacy XRAY-only schemas. The matching backend +// PR (remnawave/backend#168) adds the column and accepts the field +// on the wire, but until a new backend-contract release is published +// to npm the frontend has nothing to import. +// +// MIGRATION: once @remnawave/backend-contract publishes a release that +// includes `NodeCore`, replace every import of +// `CreateVeilAwareNodeCommand` / `UpdateVeilAwareNodeCommand` with +// `CreateNodeCommand` / `UpdateNodeCommand` from the package and +// delete this file. + +import { CreateNodeCommand, UpdateNodeCommand } from '@remnawave/backend-contract' +import { z } from 'zod' + +import { DEFAULT_NODE_CORE, NODE_CORE } from '@shared/constants/veil' + +const NodeCoreSchema = z + .enum([NODE_CORE.XRAY, NODE_CORE.VEIL] as const) + .default(DEFAULT_NODE_CORE) + +export const CreateVeilAwareNodeRequestSchema = CreateNodeCommand.RequestSchema.extend({ + core: NodeCoreSchema, +}) + +export type CreateVeilAwareNodeRequest = z.infer; + +export const UpdateVeilAwareNodeRequestSchema = UpdateNodeCommand.RequestSchema.extend({ + core: NodeCoreSchema.optional(), +}) + +export type UpdateVeilAwareNodeRequest = z.infer; diff --git a/src/shared/api/hooks/nodes/nodes.mutation.hooks.ts b/src/shared/api/hooks/nodes/nodes.mutation.hooks.ts index f6ff43d6..7dd82a32 100644 --- a/src/shared/api/hooks/nodes/nodes.mutation.hooks.ts +++ b/src/shared/api/hooks/nodes/nodes.mutation.hooks.ts @@ -14,11 +14,21 @@ import { } from '@remnawave/backend-contract' import { notifications } from '@mantine/notifications' +import { + CreateVeilAwareNodeRequestSchema, + UpdateVeilAwareNodeRequestSchema +} from '@shared/api/contracts' + import { createMutationHook } from '../../tsq-helpers' +// Body schemas point at Veil-aware extensions of the upstream +// CreateNodeCommand / UpdateNodeCommand schemas. They are pure +// supersets — every existing XRAY-only payload still parses unchanged. +// See `@shared/api/contracts/veil-node.contract` for the swap-out plan +// once @remnawave/backend-contract publishes the upstream `core` field. export const useCreateNode = createMutationHook({ endpoint: CreateNodeCommand.TSQ_url, - bodySchema: CreateNodeCommand.RequestSchema, + bodySchema: CreateVeilAwareNodeRequestSchema, responseSchema: CreateNodeCommand.ResponseSchema, requestMethod: CreateNodeCommand.endpointDetails.REQUEST_METHOD, rMutationParams: { @@ -42,7 +52,7 @@ export const useCreateNode = createMutationHook({ export const useUpdateNode = createMutationHook({ endpoint: UpdateNodeCommand.TSQ_url, - bodySchema: UpdateNodeCommand.RequestSchema, + bodySchema: UpdateVeilAwareNodeRequestSchema, responseSchema: UpdateNodeCommand.ResponseSchema, requestMethod: UpdateNodeCommand.endpointDetails.REQUEST_METHOD, rMutationParams: { diff --git a/src/shared/constants/index.ts b/src/shared/constants/index.ts index 7fdb1033..45c0f3b5 100644 --- a/src/shared/constants/index.ts +++ b/src/shared/constants/index.ts @@ -3,3 +3,4 @@ export * from './interfaces' export * from './monaco-theme' export * from './routes' export * from './theme' +export * from './veil' diff --git a/src/shared/constants/veil/index.ts b/src/shared/constants/veil/index.ts new file mode 100644 index 00000000..9b26f4e8 --- /dev/null +++ b/src/shared/constants/veil/index.ts @@ -0,0 +1 @@ +export * from './node-core' diff --git a/src/shared/constants/veil/node-core.ts b/src/shared/constants/veil/node-core.ts new file mode 100644 index 00000000..59f5ce32 --- /dev/null +++ b/src/shared/constants/veil/node-core.ts @@ -0,0 +1,26 @@ +// Node core flavours. Mirrors the `Nodes.core` enum that the panel-side +// PR adds (remnawave/backend#168) and the controller surface that +// remnawave/node#38 exposes under `/node/veil/*`. +// +// Lives client-side because @remnawave/backend-contract@2.7.2 (the +// version this branch pins) doesn't yet ship `NodeCore`; once the +// backend release that does is published, swap the imports here for +// the upstream schema and delete this file. + +export const NODE_CORE = { + XRAY: 'XRAY', + VEIL: 'VEIL', +} as const + +export type TNodeCore = (typeof NODE_CORE)[keyof typeof NODE_CORE]; + +export const NODE_CORE_VALUES: TNodeCore[] = [NODE_CORE.XRAY, NODE_CORE.VEIL] + +export const DEFAULT_NODE_CORE: TNodeCore = NODE_CORE.XRAY + +// Human-friendly labels surfaced in the Mantine `Select`. Kept here so +// the create + edit forms stay in lock-step. +export const NODE_CORE_LABELS: Record = { + XRAY: 'Xray-core', + VEIL: 'Veil (pre-alpha)', +} diff --git a/src/shared/ui/forms/nodes/base-node-form/node-vitals.card.tsx b/src/shared/ui/forms/nodes/base-node-form/node-vitals.card.tsx index 0ff6a445..e1df98a7 100644 --- a/src/shared/ui/forms/nodes/base-node-form/node-vitals.card.tsx +++ b/src/shared/ui/forms/nodes/base-node-form/node-vitals.card.tsx @@ -4,7 +4,14 @@ import { GetPubKeyCommand, UpdateNodeCommand } from '@remnawave/backend-contract' -import { TbCertificate, TbMapPin, TbPackage, TbUserCheck, TbWorld } from 'react-icons/tb' +import { + TbCertificate, + TbCpu, + TbMapPin, + TbPackage, + TbUserCheck, + TbWorld +} from 'react-icons/tb' import { ForwardRefComponent, HTMLMotionProps, Variants } from 'motion/react' import { Group, NumberInput, Select, Stack, TextInput } from '@mantine/core' import { UseFormReturnType } from '@mantine/form' @@ -13,10 +20,16 @@ import { useTranslation } from 'react-i18next' import { CopyableFieldShared } from '@shared/ui/copyable-field/copyable-field' import { BaseOverlayHeader } from '@shared/ui/overlays/base-overlay-header' +import { NODE_CORE, NODE_CORE_LABELS } from '@shared/constants/veil' import { SectionCard } from '@shared/ui/section-card' import { COUNTRIES } from './constants' +const NODE_CORE_OPTIONS = [ + { value: NODE_CORE.XRAY, label: NODE_CORE_LABELS.XRAY }, + { value: NODE_CORE.VEIL, label: NODE_CORE_LABELS.VEIL } +] + interface IProps { cardVariants: Variants form: UseFormReturnType @@ -61,6 +74,20 @@ export const NodeVitalsCard = + } + required + size="sm" + styles={{ + label: { fontWeight: 500 } + }} + {...form.getInputProps('core')} + /> + + form: UseFormReturnType isCreating: boolean onCreateNode: () => void onPrev: () => void diff --git a/src/widgets/dashboard/nodes/edit-node-by-uuid-modal/edit-node-by-uuid-modal.content.tsx b/src/widgets/dashboard/nodes/edit-node-by-uuid-modal/edit-node-by-uuid-modal.content.tsx index a42ae3dd..acea07b7 100644 --- a/src/widgets/dashboard/nodes/edit-node-by-uuid-modal/edit-node-by-uuid-modal.content.tsx +++ b/src/widgets/dashboard/nodes/edit-node-by-uuid-modal/edit-node-by-uuid-modal.content.tsx @@ -12,7 +12,12 @@ import { useGetPubKey, useUpdateNode } from '@shared/api/hooks' +import { + UpdateVeilAwareNodeRequest, + UpdateVeilAwareNodeRequestSchema +} from '@shared/api/contracts' import { BaseNodeForm } from '@shared/ui/forms/nodes/base-node-form/base-node-form' +import { DEFAULT_NODE_CORE, TNodeCore } from '@shared/constants/veil' import { bytesToGbUtil, gbToBytesUtil } from '@shared/utils/bytes' import { LoaderModalShared } from '@shared/ui/loader-modal' import { queryClient } from '@shared/api' @@ -28,10 +33,10 @@ interface IProps { export const EditNodeByUuidModalContent = (props: IProps) => { const { nodeUuid, onClose } = props - const form = useForm({ + const form = useForm({ name: 'edit-node-form', mode: 'uncontrolled', - validate: zodResolver(UpdateNodeCommand.RequestSchema.omit({ uuid: true })) + validate: zodResolver(UpdateVeilAwareNodeRequestSchema.omit({ uuid: true })) }) const { data: pubKey } = useGetPubKey() @@ -64,6 +69,13 @@ export const EditNodeByUuidModalContent = (props: IProps) => { useEffect(() => { if (fetchedNode) { + // `core` isn't on the typed Nodes schema in + // @remnawave/backend-contract@2.7.2 yet — read it through a + // narrowly-typed cast until the upstream release lands. The + // backend serialises it on every node row regardless. + const fetchedCore = + ((fetchedNode as unknown as { core?: TNodeCore }).core ?? DEFAULT_NODE_CORE) + form.initialize({ uuid: fetchedNode.uuid, countryCode: fetchedNode.countryCode, @@ -76,6 +88,7 @@ export const EditNodeByUuidModalContent = (props: IProps) => { notifyPercent: fetchedNode.notifyPercent ?? undefined, consumptionMultiplier: fetchedNode.consumptionMultiplier ?? undefined, tags: fetchedNode.tags ?? undefined, + core: fetchedCore, configProfile: { activeConfigProfileUuid: @@ -106,7 +119,7 @@ export const EditNodeByUuidModalContent = (props: IProps) => { activeConfigProfileUuid: values.configProfile?.activeConfigProfileUuid ?? '', activeInbounds: values.configProfile?.activeInbounds ?? [] } - } + } satisfies UpdateNodeCommand.Request & { core?: TNodeCore } }) }) diff --git a/src/widgets/dashboard/nodes/node-card/node-card.widget.tsx b/src/widgets/dashboard/nodes/node-card/node-card.widget.tsx index a42c0983..4d226097 100644 --- a/src/widgets/dashboard/nodes/node-card/node-card.widget.tsx +++ b/src/widgets/dashboard/nodes/node-card/node-card.widget.tsx @@ -21,8 +21,9 @@ import clsx from 'clsx' import { prettyBytesToAnyUtil, prettySiRealtimeBytesUtil } from '@shared/utils/bytes' import { getNodeResetDaysUtil, getXrayUptimeUtil } from '@shared/utils/time-utils' +import { NODE_CORE, TNodeCore } from '@shared/constants/veil' +import { VeilLogo, XrayLogo } from '@shared/ui/logos' import { faviconResolver } from '@shared/utils/misc' -import { XrayLogo } from '@shared/ui/logos' import { Logo } from '@shared/ui/logo' import { NodeStatusBadgeWidget } from '../node-status-badge' @@ -75,6 +76,12 @@ export const NodeCardWidget = memo((props: IProps) => { id: node.uuid }) + // `core` isn't on the typed Nodes schema in @remnawave/backend-contract@2.7.2. + // Cast and fall back to XRAY so the legacy fleet keeps the legacy logo. + const nodeCore: TNodeCore = + ((node as unknown as { core?: TNodeCore }).core ?? NODE_CORE.XRAY) + const CoreLogo = nodeCore === NODE_CORE.VEIL ? VeilLogo : XrayLogo + const style: CSSProperties = { transform: CSS.Transform.toString(transform), transition, @@ -333,7 +340,7 @@ export const NodeCardWidget = memo((props: IProps) => { {isOnline && ( - + { - + {node.versions ? node.versions.xray : '—'} @@ -578,7 +585,7 @@ export const NodeCardWidget = memo((props: IProps) => { )} - +