chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,15 @@
'use client'
import { type ErrorBoundaryProps, ErrorState } from '@/app/workspace/[workspaceId]/components'
export default function SettingsSectionError({ error, reset }: ErrorBoundaryProps) {
return (
<ErrorState
error={error}
reset={reset}
title='Something went wrong'
description='An unexpected error occurred. Please try again or refresh the page.'
loggerName='SettingsSectionError'
/>
)
}
@@ -0,0 +1,18 @@
import {
SettingsHeaderProvider,
SettingsHeaderShell,
} from '@/app/workspace/[workspaceId]/settings/components/settings-header/settings-header'
/**
* Persistent chrome for the settings panel pages. The header bar, title,
* description, scroll region, and centered column live in the shell and stay
* mounted across section navigation — only the body swaps. Scoped to `[section]`
* so detail routes (e.g. `secrets/[credentialId]`) keep their own chrome.
*/
export default function SettingsSectionLayout({ children }: { children: React.ReactNode }) {
return (
<SettingsHeaderProvider>
<SettingsHeaderShell>{children}</SettingsHeaderShell>
</SettingsHeaderProvider>
)
}
@@ -0,0 +1,76 @@
import { Suspense } from 'react'
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import type { Metadata } from 'next'
import { redirect } from 'next/navigation'
import { isBillingEnabled } from '@/lib/core/config/env-flags'
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
import type { SettingsSection } from '@/app/workspace/[workspaceId]/settings/navigation'
import { prefetchGeneralSettings, prefetchSubscriptionData, prefetchUserProfile } from './prefetch'
import { SettingsPage } from './settings'
/**
* Legacy settings sections that moved to top-level workspace routes.
* Old bookmarks and emails deep-link here; without the redirect the
* section renderer has no matching branch and shows an empty panel.
*/
const SETTINGS_REDIRECTS: Record<string, (workspaceId: string) => string> = {
integrations: (id) => `/workspace/${id}/integrations`,
skills: (id) => `/workspace/${id}/skills`,
}
const SECTION_TITLES: Record<string, string> = {
general: 'General',
secrets: 'Secrets',
'access-control': 'Access Control',
'audit-logs': 'Audit Logs',
apikeys: 'Sim API Keys',
byok: 'BYOK',
subscription: 'Subscription',
billing: 'Billing',
teammates: 'Teammates',
team: 'Team',
sso: 'Single Sign-On',
whitelabeling: 'Whitelabeling',
copilot: 'Chat Keys',
forks: 'Workspace Forks',
mcp: 'MCP Tools',
'custom-tools': 'Custom Tools',
'workflow-mcp-servers': 'MCP Servers',
'data-retention': 'Data Retention',
'recently-deleted': 'Recently Deleted',
debug: 'Debug',
} as const
export async function generateMetadata({
params,
}: {
params: Promise<{ section: string }>
}): Promise<Metadata> {
const { section } = await params
return { title: SECTION_TITLES[section] ?? 'Settings' }
}
export default async function SettingsSectionPage({
params,
}: {
params: Promise<{ workspaceId: string; section: string }>
}) {
const { workspaceId, section } = await params
const redirectTo = SETTINGS_REDIRECTS[section]
if (redirectTo) redirect(redirectTo(workspaceId))
const queryClient = getQueryClient()
void prefetchGeneralSettings(queryClient)
void prefetchUserProfile(queryClient)
if (isBillingEnabled) void prefetchSubscriptionData(queryClient)
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<Suspense fallback={null}>
<SettingsPage section={section as SettingsSection} />
</Suspense>
</HydrationBoundary>
)
}
@@ -0,0 +1,78 @@
import type { QueryClient } from '@tanstack/react-query'
import { headers } from 'next/headers'
import { getSession } from '@/lib/auth'
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
import { getUserProfile, getUserSettings } from '@/lib/users/queries'
import {
GENERAL_SETTINGS_STALE_TIME,
generalSettingsKeys,
mapGeneralSettingsResponse,
} from '@/hooks/queries/general-settings'
import { SUBSCRIPTION_DATA_STALE_TIME, subscriptionKeys } from '@/hooks/queries/subscription'
import {
mapUserProfileResponse,
USER_PROFILE_STALE_TIME,
userProfileKeys,
} from '@/hooks/queries/user-profile'
/**
* Prefetch general settings server-side via the shared data layer.
* Uses the same query keys as the client `useGeneralSettings` hook
* so data is shared via HydrationBoundary.
*/
export function prefetchGeneralSettings(queryClient: QueryClient) {
return queryClient.prefetchQuery({
queryKey: generalSettingsKeys.settings(),
queryFn: async () => {
const session = await getSession()
const data = await getUserSettings(session?.user?.id ?? null)
return mapGeneralSettingsResponse(data)
},
staleTime: GENERAL_SETTINGS_STALE_TIME,
})
}
/**
* Prefetch subscription data server-side. Unlike the other prefetches this goes
* through the internal billing API rather than calling the data layer directly:
* the billing summary contains `Date` fields (and an untyped `metadata` blob) that
* `NextResponse.json` serializes to the string wire shape the client caches. Going
* through the route yields that exact shape, avoiding a Date-vs-string mismatch
* between server-hydrated and client-fetched data. Uses the same query key as the
* client `useSubscriptionData` hook (with includeOrg=false) so data is shared via
* HydrationBoundary.
*/
export function prefetchSubscriptionData(queryClient: QueryClient) {
return queryClient.prefetchQuery({
queryKey: subscriptionKeys.user(false),
queryFn: async () => {
const h = await headers()
const cookie = h.get('cookie')
const response = await fetch(`${getInternalApiBaseUrl()}/api/billing?context=user`, {
headers: cookie ? { cookie } : {},
})
if (!response.ok) throw new Error(`Subscription prefetch failed: ${response.status}`)
return response.json()
},
staleTime: SUBSCRIPTION_DATA_STALE_TIME,
})
}
/**
* Prefetch user profile server-side via the shared data layer.
* Uses the same query keys as the client `useUserProfile` hook
* so data is shared via HydrationBoundary.
*/
export function prefetchUserProfile(queryClient: QueryClient) {
return queryClient.prefetchQuery({
queryKey: userProfileKeys.profile(),
queryFn: async () => {
const session = await getSession()
if (!session?.user?.id) throw new Error('Unauthorized')
const user = await getUserProfile(session.user.id)
if (!user) throw new Error('User not found')
return mapUserProfileResponse(user)
},
staleTime: USER_PROFILE_STALE_TIME,
})
}
@@ -0,0 +1,65 @@
import { parseAsString, parseAsStringLiteral } from 'nuqs/server'
/**
* Co-located, typed URL query-param definitions for the settings section pages.
* The client hook consumes this typed param definition as the single source of
* truth.
*
* `mcpServerId` deep-links the MCP settings tab to a specific server so the row
* can be focused/opened from a shared link.
*/
export const mcpServerIdParam = {
key: 'mcpServerId',
parser: parseAsString,
} as const
/** Opening a server is a destination → push to history; clear on close. */
export const mcpServerIdUrlKeys = {
history: 'push',
clearOnDefault: true,
} as const
/**
* `fork-id` deep-links the Forks settings tab to a specific fork's detail
* sub-view (mirrors `mcpServerId` on the MCP tab).
*/
export const forkIdParam = {
key: 'fork-id',
parser: parseAsString,
} as const
/** Opening a fork's detail is a destination → push to history; clear on close. */
export const forkIdUrlKeys = {
history: 'push',
clearOnDefault: true,
} as const
/**
* `fork-view` deep-links the Forks settings tab to its workspace-scoped Activity
* view (opened from the page header's "See activity" action).
*/
export const forkViewParam = {
key: 'fork-view',
parser: parseAsStringLiteral(['activity'] as const),
} as const
/** Opening the activity view is a destination → push to history; clear on close. */
export const forkViewUrlKeys = {
history: 'push',
clearOnDefault: true,
} as const
/**
* `fork-direction` is the sync direction (push/pull) on the parent fork's detail
* page — shareable view state, so a copied link opens the same side of the sync.
*/
export const forkSyncDirectionParam = {
key: 'fork-direction',
parser: parseAsStringLiteral(['push', 'pull'] as const).withDefault('push'),
} as const
/** Toggling direction is in-place view state → replace history; clear at the push default. */
export const forkSyncDirectionUrlKeys = {
history: 'replace',
clearOnDefault: true,
} as const
@@ -0,0 +1,151 @@
'use client'
import { useEffect } from 'react'
import dynamic from 'next/dynamic'
import { usePostHog } from 'posthog-js/react'
import { useSession } from '@/lib/auth/auth-client'
import { captureEvent } from '@/lib/posthog/client'
import { General } from '@/app/workspace/[workspaceId]/settings/components/general/general'
import { SettingsSectionProvider } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import { useSettingsBeforeUnload } from '@/app/workspace/[workspaceId]/settings/hooks/use-settings-before-unload'
import type { SettingsSection } from '@/app/workspace/[workspaceId]/settings/navigation'
import { isBillingEnabled } from '@/app/workspace/[workspaceId]/settings/navigation'
const Admin = dynamic(() =>
import('@/app/workspace/[workspaceId]/settings/components/admin/admin').then((m) => m.Admin)
)
const ApiKeys = dynamic(() =>
import('@/app/workspace/[workspaceId]/settings/components/api-keys/api-keys').then(
(m) => m.ApiKeys
)
)
const BYOK = dynamic(() =>
import('@/app/workspace/[workspaceId]/settings/components/byok/byok').then((m) => m.BYOK)
)
const Copilot = dynamic(() =>
import('@/app/workspace/[workspaceId]/settings/components/copilot/copilot').then((m) => m.Copilot)
)
const Forks = dynamic(() => import('@/ee/workspace-forking/components/forks').then((m) => m.Forks))
const Secrets = dynamic(() =>
import('@/app/workspace/[workspaceId]/settings/components/secrets/secrets').then((m) => m.Secrets)
)
const CustomTools = dynamic(() =>
import('@/app/workspace/[workspaceId]/settings/components/custom-tools/custom-tools').then(
(m) => m.CustomTools
)
)
const Inbox = dynamic(() =>
import('@/app/workspace/[workspaceId]/settings/components/inbox/inbox').then((m) => m.Inbox)
)
const MCP = dynamic(() =>
import('@/app/workspace/[workspaceId]/settings/components/mcp/mcp').then((m) => m.MCP)
)
const Mothership = dynamic(() =>
import('@/app/workspace/[workspaceId]/settings/components/mothership/mothership').then(
(m) => m.Mothership
)
)
const RecentlyDeleted = dynamic(() =>
import(
'@/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted'
).then((m) => m.RecentlyDeleted)
)
const Billing = dynamic(() =>
import('@/app/workspace/[workspaceId]/settings/components/billing/billing').then((m) => m.Billing)
)
const Teammates = dynamic(() =>
import('@/app/workspace/[workspaceId]/settings/components/teammates/teammates').then(
(m) => m.Teammates
)
)
const TeamManagement = dynamic(() =>
import('@/app/workspace/[workspaceId]/settings/components/team-management/team-management').then(
(m) => m.TeamManagement
)
)
const WorkflowMcpServers = dynamic(() =>
import(
'@/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/workflow-mcp-servers'
).then((m) => m.WorkflowMcpServers)
)
const AccessControl = dynamic(() =>
import('@/ee/access-control/components/access-control').then((m) => m.AccessControl)
)
const CustomBlocks = dynamic(() =>
import('@/ee/custom-blocks/components/custom-blocks').then((m) => m.CustomBlocks)
)
const AuditLogs = dynamic(() =>
import('@/ee/audit-logs/components/audit-logs').then((m) => m.AuditLogs)
)
const SSO = dynamic(() => import('@/ee/sso/components/sso-settings').then((m) => m.SSO))
const DataRetentionSettings = dynamic(() =>
import('@/ee/data-retention/components/data-retention-settings').then(
(m) => m.DataRetentionSettings
)
)
const DataDrainsSettings = dynamic(() =>
import('@/ee/data-drains/components/data-drains-settings').then((m) => m.DataDrainsSettings)
)
const WhitelabelingSettings = dynamic(
() =>
import('@/ee/whitelabeling/components/whitelabeling-settings').then(
(m) => m.WhitelabelingSettings
),
{ ssr: false }
)
interface SettingsPageProps {
section: SettingsSection
}
export function SettingsPage({ section }: SettingsPageProps) {
const { data: session, isPending: sessionLoading } = useSession()
const posthog = usePostHog()
useSettingsBeforeUnload()
const isAdminRole = session?.user?.role === 'admin'
const normalizedSection: SettingsSection =
(section as string) === 'subscription' ? 'billing' : section
const effectiveSection =
!isBillingEnabled && (normalizedSection === 'billing' || normalizedSection === 'organization')
? 'general'
: normalizedSection === 'admin' && !sessionLoading && !isAdminRole
? 'general'
: normalizedSection === 'mothership' && !sessionLoading && !isAdminRole
? 'general'
: normalizedSection
useEffect(() => {
if (sessionLoading) return
captureEvent(posthog, 'settings_tab_viewed', { section: effectiveSection })
}, [effectiveSection, sessionLoading, posthog])
return (
<SettingsSectionProvider section={effectiveSection}>
{effectiveSection === 'general' && <General />}
{effectiveSection === 'secrets' && <Secrets />}
{effectiveSection === 'access-control' && <AccessControl />}
{effectiveSection === 'custom-blocks' && <CustomBlocks />}
{effectiveSection === 'audit-logs' && <AuditLogs />}
{effectiveSection === 'apikeys' && <ApiKeys />}
{isBillingEnabled && effectiveSection === 'billing' && <Billing />}
{effectiveSection === 'teammates' && <Teammates />}
{isBillingEnabled && effectiveSection === 'organization' && <TeamManagement />}
{effectiveSection === 'sso' && <SSO />}
{effectiveSection === 'data-retention' && <DataRetentionSettings />}
{effectiveSection === 'data-drains' && <DataDrainsSettings />}
{effectiveSection === 'whitelabeling' && <WhitelabelingSettings />}
{effectiveSection === 'byok' && <BYOK />}
{effectiveSection === 'copilot' && <Copilot />}
{effectiveSection === 'mcp' && <MCP />}
{effectiveSection === 'forks' && <Forks />}
{effectiveSection === 'custom-tools' && <CustomTools />}
{effectiveSection === 'workflow-mcp-servers' && <WorkflowMcpServers />}
{effectiveSection === 'inbox' && <Inbox />}
{effectiveSection === 'recently-deleted' && <RecentlyDeleted />}
{effectiveSection === 'admin' && <Admin />}
{effectiveSection === 'mothership' && <Mothership />}
</SettingsSectionProvider>
)
}