feat: интеграция с VPS Tracker
Build, Test, and Push CFDM Docker Image / test (push) Failing after 3m36s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Has been skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Has been skipped
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped

Исходящий sync bindings после updateConfig, настройки в app_settings, страница Интеграции, приём событий vps_down для DNS failover.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-06-30 16:45:17 +07:00
co-authored by Cursor
parent 859918fae2
commit 96783386e0
32 changed files with 2116 additions and 243 deletions
@@ -0,0 +1,7 @@
import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/_auth/settings/')({
beforeLoad: () => {
throw redirect({ to: '/settings/integrations' })
},
})
@@ -0,0 +1,74 @@
import { createFileRoute } from '@tanstack/react-router'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import type { AppSettingsPatch } from '@cfdm/shared'
import { api } from '@/lib/api-client'
import { QueryState } from '@/components/query-state'
import { AppSwitcherEditor } from '@/components/integrations/app-switcher-editor'
import {
VpsTrackerIntegrationCard,
type AppSettingsView,
} from '@/components/integrations/vps-tracker-integration-card'
import { DEFAULT_APP_SWITCHER_CONFIG } from '@/lib/app-switcher-config'
import { appSwitcherQueryKey } from '@/queries/app-switcher'
type SettingsResponse = AppSettingsView & {
id: string
appSwitcher: typeof DEFAULT_APP_SWITCHER_CONFIG
}
export const Route = createFileRoute('/_auth/settings/integrations')({
component: SettingsIntegrationsPage,
})
function SettingsIntegrationsPage() {
const queryClient = useQueryClient()
const { data, isLoading, isError, error, refetch } = useQuery({
queryKey: ['app-settings'],
queryFn: () => api.get<SettingsResponse>('/api/v1/settings'),
})
const saveMut = useMutation({
mutationFn: (patch: AppSettingsPatch) =>
api.patch<SettingsResponse>('/api/v1/settings', patch),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['app-settings'] })
await queryClient.invalidateQueries({ queryKey: appSwitcherQueryKey })
toast.success('Настройки сохранены')
},
onError: () => toast.error('Не удалось сохранить'),
})
return (
<QueryState
isLoading={isLoading}
isError={isError}
error={error}
onRetry={() => refetch()}
>
{data ? (
<div className="flex flex-col gap-4">
<AppSwitcherEditor
defaultValues={data.appSwitcher ?? DEFAULT_APP_SWITCHER_CONFIG}
isSaving={saveMut.isPending}
onSave={(appSwitcher) => saveMut.mutate({ appSwitcher })}
/>
<VpsTrackerIntegrationCard
settings={data}
isSaving={saveMut.isPending}
onSave={(values) => {
const patch: AppSettingsPatch = {
vpsTrackerUrl: values.vpsTrackerUrl,
vpsTrackerSyncEnabled: values.vpsTrackerSyncEnabled,
}
const token = values.vpsTrackerIntegrationToken?.trim()
if (token) patch.vpsTrackerIntegrationToken = token
saveMut.mutate(patch)
}}
/>
</div>
) : null}
</QueryState>
)
}
@@ -0,0 +1,40 @@
import { createFileRoute, Link, Outlet, useRouterState } from '@tanstack/react-router'
import { cn } from '@cfdm/ui/lib/utils'
import { PageShell } from '@/components/page-shell'
import { PageHeader } from '@/components/page-header'
export const Route = createFileRoute('/_auth/settings')({
component: SettingsLayout,
})
const TABS = [{ to: '/settings/integrations', label: 'Интеграции' }] as const
function SettingsLayout() {
const pathname = useRouterState({ select: (s) => s.location.pathname })
return (
<PageShell>
<PageHeader title="Настройки" description="Интеграции с другими приложениями" />
<nav className="flex gap-1 border-b pb-0">
{TABS.map((tab) => {
const active = pathname.startsWith(tab.to)
return (
<Link
key={tab.to}
to={tab.to}
className={cn(
'rounded-t-md px-4 py-2 text-sm font-medium transition-colors',
active
? 'border border-b-0 bg-background text-foreground'
: 'text-muted-foreground hover:text-foreground',
)}
>
{tab.label}
</Link>
)
})}
</nav>
<Outlet />
</PageShell>
)
}