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
superpowers/
3 changes: 2 additions & 1 deletion dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"preview": "vite preview",
"gen:api": "orval",
"wait-port": "wait-port http://localhost:$UVICORN_PORT/openapi.json",
"wait-port-gen-api": "bun run wait-port && bun run gen:api"
"wait-port-gen-api": "bun run wait-port && bun run gen:api",
"test": "node --test 'src/**/*.test.ts'"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
Expand Down
8 changes: 7 additions & 1 deletion dashboard/public/statics/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2218,6 +2218,7 @@
"balancers": "Balancers",
"dns": "DNS",
"bindings": "Bindings",
"api": "API",
"advanced": "Advanced",
"interface": "Interface"
},
Expand All @@ -2229,7 +2230,12 @@
"routing": "Traffic rules and routing",
"balancers": "Outbound load balancing setup",
"dns": "DNS resolver and server config",
"bindings": "Inbound tag fallbacks and exclusions"
"bindings": "Inbound tag fallbacks and exclusions",
"api": "Xray gRPC API services"
},
"api": {
"title": "API Services",
"hint": "gRPC API services exposed by this core. The node always enables the required services; enable optional ones as needed."
},
"wg": {
"interfaceBlurb": "WireGuard interface settings and key material for this core.",
Expand Down
8 changes: 7 additions & 1 deletion dashboard/public/statics/locales/fa.json
Original file line number Diff line number Diff line change
Expand Up @@ -2133,6 +2133,7 @@
"balancers": "متعادل‌کننده‌ها",
"dns": "DNS",
"bindings": "اتصال‌ها",
"api": "API",
"advanced": "پیشرفته",
"interface": "رابط"
},
Expand All @@ -2144,7 +2145,12 @@
"routing": "قوانین مسیریابی و ترافیک",
"balancers": "استراتژی‌های متعادل‌سازی بار",
"dns": "پیکربندی DNS و حل‌کننده",
"bindings": "پشتیبان و مستثنا برای برچسب‌های ورودی"
"bindings": "پشتیبان و مستثنا برای برچسب‌های ورودی",
"api": "سرویس‌های gRPC API مربوط به Xray"
},
"api": {
"title": "سرویس‌های API",
"hint": "سرویس‌های gRPC API که این هسته ارائه می‌دهد. گره همیشه سرویس‌های موردنیاز را فعال می‌کند؛ موارد اختیاری را در صورت نیاز فعال کنید."
},
"wg": {
"interfaceBlurb": "تنظیمات رابط وایرگارد و مواد کلیدی این هسته.",
Expand Down
8 changes: 7 additions & 1 deletion dashboard/public/statics/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -2107,6 +2107,7 @@
"balancers": "Балансировщики",
"dns": "DNS",
"bindings": "Привязки",
"api": "API",
"advanced": "Дополнительно",
"interface": "Интерфейс"
},
Expand All @@ -2118,7 +2119,12 @@
"routing": "Правила маршрутизации трафика",
"balancers": "Стратегии балансировки нагрузки",
"dns": "Настройки DNS и резолвера",
"bindings": "Резервные и исключённые теги входящих"
"bindings": "Резервные и исключённые теги входящих",
"api": "Сервисы gRPC API Xray"
},
"api": {
"title": "Сервисы API",
"hint": "Сервисы gRPC API, предоставляемые этим ядром. Узел всегда включает обязательные сервисы; необязательные включайте по мере необходимости."
},
"wg": {
"interfaceBlurb": "Настройки интерфейса WireGuard и ключи для этого ядра.",
Expand Down
8 changes: 7 additions & 1 deletion dashboard/public/statics/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -2178,6 +2178,7 @@
"balancers": "负载均衡",
"dns": "DNS",
"bindings": "绑定",
"api": "API",
"advanced": "高级",
"interface": "接口"
},
Expand All @@ -2189,7 +2190,12 @@
"routing": "路由规则与流量策略",
"balancers": "负载均衡与调度策略",
"dns": "DNS 解析与解析器",
"bindings": "入站标签回退与排除"
"bindings": "入站标签回退与排除",
"api": "Xray gRPC API 服务"
},
"api": {
"title": "API 服务",
"hint": "此核心提供的 gRPC API 服务。节点始终启用必需的服务;可按需启用可选服务。"
},
"wg": {
"interfaceBlurb": "此核心的 WireGuard 接口与密钥设置。",
Expand Down
10 changes: 7 additions & 3 deletions dashboard/src/components/layout/page-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import { useLocation } from 'react-router'

interface PageHeaderProps {
title: string
/** Fallback text when the `title` i18n key is missing (e.g. a stale cached locale bundle). */
titleDefault?: string
description?: string
/** Fallback text when the `description` i18n key is missing. */
descriptionDefault?: string
buttonText?: string
onButtonClick?: () => void
buttonIcon?: LucideIcon
Expand All @@ -19,7 +23,7 @@ interface PageHeaderProps {
className?: string
}

export default function PageHeader({ title, description, buttonText, onButtonClick, buttonIcon: Icon = Plus, buttonTooltip, tutorialUrl, className }: PageHeaderProps) {
export default function PageHeader({ title, titleDefault, description, descriptionDefault, buttonText, onButtonClick, buttonIcon: Icon = Plus, buttonTooltip, tutorialUrl, className }: PageHeaderProps) {
const { t } = useTranslation()
const dir = useDirDetection()
const location = useLocation()
Expand All @@ -32,7 +36,7 @@ export default function PageHeader({ title, description, buttonText, onButtonCli
<Snowfall className="snowfall--header" />
<div className="relative z-10 flex min-w-0 flex-1 flex-col gap-y-1">
<div className="flex min-w-0 items-center gap-2.5">
<h1 className="truncate text-lg font-medium sm:text-xl">{t(title)}</h1>
<h1 className="truncate text-lg font-medium sm:text-xl">{t(title, titleDefault ? { defaultValue: titleDefault } : undefined)}</h1>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
Expand All @@ -52,7 +56,7 @@ export default function PageHeader({ title, description, buttonText, onButtonCli
</Tooltip>
</TooltipProvider>
</div>
{description && <span className="text-muted-foreground text-xs whitespace-normal sm:text-sm">{t(description)}</span>}
{description && <span className="text-muted-foreground text-xs whitespace-normal sm:text-sm">{t(description, descriptionDefault ? { defaultValue: descriptionDefault } : undefined)}</span>}
</div>
{buttonText && onButtonClick && (
<div className="relative z-10 shrink-0">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Checkbox } from '@/components/ui/checkbox'
import { useCoreEditorStore } from '@/features/core-editor/state/core-editor-store'
import { findUnknownApiServices, getSelectedOptional, OPTIONAL_API_SERVICES, REQUIRED_API_SERVICES, setRawOptionalService } from '@/lib/xray-api-services'
import type { Profile } from '@pasarguard/xray-config-kit'
import { Webhook } from 'lucide-react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'

// The structured editor keeps unmodeled top-level keys (incl. `api`) on
// `profile.raw.topLevel` (see UNMODELED_TOP_LEVEL_KEYS_TO_PRESERVE in xray-adapter.ts),
// which is structurally a config object for the api-services helpers.
function readTopLevel(profile: Profile): Record<string, unknown> {
return (profile.raw?.topLevel ?? {}) as Record<string, unknown>
}

function withApiService(profile: Profile, service: string, enabled: boolean): Profile {
return { ...profile, raw: setRawOptionalService(profile.raw, service, enabled) } as Profile
}

export function XrayApiSection() {
const { t } = useTranslation()
const profile = useCoreEditorStore(s => s.xrayProfile)
const updateXrayProfile = useCoreEditorStore(s => s.updateXrayProfile)

const { selected, unknown } = useMemo(() => {
const topLevel = profile ? readTopLevel(profile) : {}
return {
selected: getSelectedOptional(topLevel),
unknown: findUnknownApiServices(topLevel),
}
}, [profile])

if (!profile) return null

const toggle = (service: string, enabled: boolean) => {
updateXrayProfile(p => withApiService(p, service, enabled))
}

return (
<div className="space-y-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-sm font-medium">
<Webhook className="text-muted-foreground h-4 w-4 shrink-0" aria-hidden />
{t('coreEditor.api.title', { defaultValue: 'API Services' })}
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-muted-foreground text-xs">
{t('coreEditor.api.hint', {
defaultValue: 'gRPC API services exposed by this core. The node always enables the required services; enable optional ones as needed.',
})}
</p>
<div className="space-y-2 rounded-md border p-3">
{REQUIRED_API_SERVICES.map(svc => (
<label key={svc} className="flex items-center gap-2 opacity-60">
<Checkbox checked disabled />
<span className="text-sm">{svc}</span>
<span className="text-muted-foreground text-xs">{t('coreEditor.api.alwaysOn', { defaultValue: 'always on' })}</span>
</label>
))}
{OPTIONAL_API_SERVICES.map(svc => (
<label key={svc} className="flex cursor-pointer items-center gap-2">
<Checkbox checked={selected.includes(svc)} onCheckedChange={checked => toggle(svc, checked === true)} />
<span className="text-sm">{svc}</span>
</label>
))}
</div>
{unknown.length > 0 && (
<p className="text-destructive text-xs">
{t('coreEditor.api.unknown', {
defaultValue: 'Unrecognized API service(s): {{names}}. Fix them in the Advanced tab.',
names: unknown.join(', '),
})}
</p>
)}
</CardContent>
</Card>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { XrayInboundTagSelectors } from '@/features/core-editor/components/shared/xray-inbound-tag-selectors'
import { XrayAdvancedSection } from '@/features/core-editor/components/xray/xray-advanced-section'
import { XrayApiSection } from '@/features/core-editor/components/xray/xray-api-section'
import { XrayBalancersSection } from '@/features/core-editor/components/xray/xray-balancers-section'
import { XrayDnsSection } from '@/features/core-editor/components/xray/xray-dns-section'
import { XrayInboundsSection } from '@/features/core-editor/components/xray/xray-inbounds-section'
Expand Down Expand Up @@ -31,6 +32,7 @@ export function XrayCoreEditor({ headerAddPulse, headerAddEpoch }: XrayCoreEdito
{section === 'balancers' && <XrayBalancersSection headerAddPulse={headerAddPulse} headerAddEpoch={headerAddEpoch} />}
{section === 'dns' && <XrayDnsSection headerAddPulse={headerAddPulse} headerAddEpoch={headerAddEpoch} />}
{section === 'bindings' && <XrayInboundTagSelectors inboundTags={inboundTags} fallbackTags={fallbacks} excludedTags={excludes} onFallbackChange={setFallbacks} onExcludedChange={setExcludes} />}
{section === 'api' && <XrayApiSection />}
{section === 'advanced' && <XrayAdvancedSection />}
</div>
)
Expand Down
3 changes: 2 additions & 1 deletion dashboard/src/features/core-editor/kit/core-section-nav.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { LucideIcon } from 'lucide-react'
import { ArrowDownToLine, ArrowUpFromLine, Braces, Cable, Globe, Link2, Scale, Waypoints } from 'lucide-react'
import { ArrowDownToLine, ArrowUpFromLine, Braces, Cable, Globe, Link2, Scale, Waypoints, Webhook } from 'lucide-react'
import type { WgCoreSection, XrayCoreSection } from '@/features/core-editor/state/core-editor-store'

export type XraySectionNavItem = {
Expand All @@ -23,6 +23,7 @@ export const XRAY_CORE_SECTION_NAV: XraySectionNavItem[] = [
{ id: 'balancers', labelKey: 'coreEditor.section.balancers', defaultLabel: 'Balancers', icon: Scale },
{ id: 'dns', labelKey: 'coreEditor.section.dns', defaultLabel: 'DNS', icon: Globe },
{ id: 'bindings', labelKey: 'coreEditor.section.bindings', defaultLabel: 'Bindings', icon: Link2 },
{ id: 'api', labelKey: 'coreEditor.section.api', defaultLabel: 'API', icon: Webhook },
{ id: 'advanced', labelKey: 'coreEditor.section.advanced', defaultLabel: 'Advanced', icon: Braces },
]

Expand Down
29 changes: 24 additions & 5 deletions dashboard/src/features/core-editor/routes/core-editor-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ import useDirDetection from '@/hooks/use-dir-detection'

type LoadingCoreKind = 'xray' | 'wg'

type SectionHeaderConfig = {
title: string
/** Fallback shown when the `title` key is missing from a stale cached locale bundle. */
titleDefault?: string
description?: string
descriptionDefault?: string
buttonText?: string
}

function loadingSectionPageHeaderProps(coreKind?: LoadingCoreKind): { title: string; description?: string } {
if (coreKind === 'wg') {
return {
Expand Down Expand Up @@ -461,10 +470,10 @@ export default function CoreEditorPage() {
</div>
)

const sectionHeaderConfig = useMemo(() => {
const sectionHeaderConfig: SectionHeaderConfig | undefined = useMemo(() => {
if (kind === 'wg') {
const section = activeSection as WgCoreSection
return {
const map: Record<WgCoreSection, SectionHeaderConfig> = {
interface: {
title: 'coreEditor.section.interface',
description: 'coreEditor.sectionDesc.wgInterface',
Expand All @@ -473,11 +482,12 @@ export default function CoreEditorPage() {
title: 'coreEditor.section.advanced',
description: 'coreEditor.sectionDesc.advanced',
},
}[section]
}
return map[section]
}

const section = activeSection as XrayCoreSection
return {
const map: Record<XrayCoreSection, SectionHeaderConfig> = {
inbounds: {
title: 'coreEditor.section.inbounds',
description: 'coreEditor.sectionDesc.inbounds',
Expand Down Expand Up @@ -506,11 +516,18 @@ export default function CoreEditorPage() {
title: 'coreEditor.section.bindings',
description: 'coreEditor.sectionDesc.bindings',
},
api: {
title: 'coreEditor.section.api',
titleDefault: 'API',
description: 'coreEditor.sectionDesc.api',
descriptionDefault: 'Xray gRPC API services',
},
advanced: {
title: 'coreEditor.section.advanced',
description: 'coreEditor.sectionDesc.advanced',
},
}[section]
}
return map[section]
}, [kind, activeSection])

if (!isNew && validId && isLoading) {
Expand Down Expand Up @@ -559,7 +576,9 @@ export default function CoreEditorPage() {
sectionHeaderConfig ? (
<PageHeader
title={sectionHeaderConfig.title}
titleDefault={sectionHeaderConfig.titleDefault}
description={sectionHeaderConfig.description}
descriptionDefault={sectionHeaderConfig.descriptionDefault}
className="flex-wrap gap-x-3 gap-y-2 py-2.5 sm:gap-4 sm:py-4 md:pt-6"
buttonText={sectionHeaderConfig.buttonText}
onButtonClick={sectionHeaderConfig.buttonText ? () => setHeaderAddPulse(p => ({ target: String(activeSection), n: p.n + 1 })) : undefined}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { apiCoreTypeToKind } from '../kit/core-kind'
import { createNewXrayProfile, importRawToProfile, profileToPersistedConfig } from '../kit/xray-adapter'
import { createNewWireGuardDraft, draftToPersistedConfig, wireGuardConfigToDraft } from '../kit/wireguard-adapter'

export type XrayCoreSection = 'bindings' | 'inbounds' | 'outbounds' | 'routing' | 'balancers' | 'dns' | 'advanced'
export type XrayCoreSection = 'bindings' | 'inbounds' | 'outbounds' | 'routing' | 'balancers' | 'dns' | 'api' | 'advanced'

export type WgCoreSection = 'interface' | 'advanced'

Expand Down
Loading