Skip to content

Commit 4d115b3

Browse files
feat(settings): add a self-host section with the managed Chat keys link
Self-hosters had no in-app pointer to the managed service that issues their Chat keys. New Settings > System > Self-host section, gated on `requiresSelfHosted` so it is absent on hosted Sim, containing only that link.
1 parent 7ede958 commit 4d115b3

7 files changed

Lines changed: 147 additions & 2 deletions

File tree

apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const WORKSPACE_SECTION_MAP: Partial<Record<SettingsSection, WorkspaceSettingsSe
5858
'recently-deleted': 'recently-deleted',
5959
forks: 'forks',
6060
'custom-blocks': 'custom-blocks',
61+
'self-host': 'self-host',
6162
}
6263

6364
const ORGANIZATION_SECTION_MAP: Partial<Record<SettingsSection, OrganizationSettingsSection>> = {

apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ const RecentlyDeleted = dynamic(() =>
5555
'@/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted'
5656
).then((m) => m.RecentlyDeleted)
5757
)
58+
const SelfHost = dynamic(() =>
59+
import('@/app/workspace/[workspaceId]/settings/components/self-host/self-host').then(
60+
(m) => m.SelfHost
61+
)
62+
)
5863
const Billing = dynamic(() =>
5964
import('@/app/workspace/[workspaceId]/settings/components/billing/billing').then((m) => m.Billing)
6065
)
@@ -200,6 +205,7 @@ export function SettingsPage({ section }: SettingsPageProps) {
200205
{effectiveSection === 'workflow-mcp-servers' && <WorkflowMcpServers />}
201206
{effectiveSection === 'inbox' && <Inbox />}
202207
{effectiveSection === 'recently-deleted' && <RecentlyDeleted />}
208+
{effectiveSection === 'self-host' && <SelfHost />}
203209
{effectiveSection === 'admin' && <Admin />}
204210
{effectiveSection === 'mothership' && <Mothership />}
205211
</SettingsSectionProvider>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { renderToStaticMarkup } from 'react-dom/server'
5+
import { describe, expect, it } from 'vitest'
6+
import { SelfHost } from '@/app/workspace/[workspaceId]/settings/components/self-host/self-host'
7+
8+
describe('SelfHost settings section', () => {
9+
it('links to the managed Chat keys page', () => {
10+
const markup = renderToStaticMarkup(<SelfHost />)
11+
12+
expect(markup).toContain('href="https://www.sim.ai/selfhost/settings/chat-keys"')
13+
})
14+
15+
/**
16+
* The section is deliberately only the managed link — no status readouts,
17+
* capability inventories, or environment-variable listings.
18+
*/
19+
it('renders exactly one link and no other controls', () => {
20+
const markup = renderToStaticMarkup(<SelfHost />)
21+
22+
expect(markup.match(/<a /g)).toHaveLength(1)
23+
expect(markup).not.toContain('<button')
24+
expect(markup).not.toContain('<input')
25+
})
26+
})
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use client'
2+
3+
import { ChipLink } from '@sim/emcn'
4+
import { SITE_URL } from '@/lib/core/utils/urls'
5+
import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
6+
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
7+
8+
/** Chat keys are issued by the managed service, not by this deployment. */
9+
const CHAT_KEYS_HREF = `${SITE_URL}/selfhost/settings/chat-keys`
10+
11+
export function SelfHost() {
12+
return (
13+
<SettingsPanel>
14+
<SettingsSection label='Chat keys'>
15+
<div className='flex items-center justify-between px-2'>
16+
<div className='flex flex-col justify-center gap-[1px]'>
17+
<span className='text-[var(--text-body)] text-sm'>Managed on sim.ai</span>
18+
<span className='text-[var(--text-muted)] text-caption'>
19+
Manage the model-provider keys that power Chat on this deployment.
20+
</span>
21+
</div>
22+
<ChipLink href={CHAT_KEYS_HREF} target='_blank' rel='noopener noreferrer'>
23+
Manage keys
24+
</ChipLink>
25+
</div>
26+
</SettingsSection>
27+
</SettingsPanel>
28+
)
29+
}

apps/sim/app/workspace/[workspaceId]/settings/navigation.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ describe('unified settings navigation', () => {
5151
{ id: 'sandboxes', label: 'Sandboxes', section: 'workspace' },
5252
{ id: 'inbox', label: 'Sim Mailer', section: 'workspace' },
5353
{ id: 'recently-deleted', label: 'Recently deleted', section: 'workspace' },
54+
{ id: 'self-host', label: 'Self-host', section: 'workspace' },
5455
{ id: 'sso', label: 'Single sign-on', section: 'organization' },
5556
{ id: 'sessions', label: 'Session policies', section: 'organization' },
5657
{ id: 'data-retention', label: 'Data retention', section: 'organization' },
@@ -87,6 +88,7 @@ describe('unified settings navigation', () => {
8788
'apikeys',
8889
'sandboxes',
8990
'recently-deleted',
91+
'self-host',
9092
])
9193
expect(idsForSection('organization')).toEqual([
9294
'organization',

apps/sim/components/settings/navigation.test.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* @vitest-environment node
33
*/
4-
import { resetEnvMock, setEnv } from '@sim/testing'
4+
import { resetEnvFlagsMock, resetEnvMock, setEnv, setEnvFlags } from '@sim/testing'
55
import { afterAll, beforeEach, describe, expect, it } from 'vitest'
66
import {
77
ACCOUNT_SETTINGS_ITEMS,
@@ -32,9 +32,13 @@ import {
3232
*/
3333
beforeEach(() => {
3434
setEnv({ NEXT_PUBLIC_SANDBOX_ENABLED: 'true', NEXT_PUBLIC_E2B_ENABLED: undefined })
35+
resetEnvFlagsMock()
3536
})
3637

37-
afterAll(resetEnvMock)
38+
afterAll(() => {
39+
resetEnvMock()
40+
resetEnvFlagsMock()
41+
})
3842

3943
describe('settings navigation boundaries', () => {
4044
it('preserves the order of all four settings catalogs', () => {
@@ -58,6 +62,7 @@ describe('settings navigation boundaries', () => {
5862
'sandboxes',
5963
'inbox',
6064
'recently-deleted',
65+
'self-host',
6166
'sso',
6267
'sessions',
6368
'data-retention',
@@ -99,6 +104,7 @@ describe('settings navigation boundaries', () => {
99104
'recently-deleted',
100105
'forks',
101106
'custom-blocks',
107+
'self-host',
102108
])
103109
})
104110

@@ -127,6 +133,50 @@ describe('settings navigation boundaries', () => {
127133
).not.toContain('sandboxes')
128134
})
129135

136+
/**
137+
* The Self-host section links out to the managed service that issues this
138+
* deployment's Chat keys. On Sim Cloud that surface is reached from the
139+
* account plane instead, so the section must not exist there at all — in the
140+
* sidebar catalog or in the workspace-plane gate the route consults.
141+
*/
142+
it('shows the Self-host section only on a self-hosted deployment', () => {
143+
setEnvFlags({ isHosted: false })
144+
145+
expect(buildUnifiedSettingsNavigation().map(({ id }) => id)).toContain('self-host')
146+
expect(
147+
resolveWorkspaceNavigation({
148+
permission: 'admin',
149+
permissionConfig: {},
150+
entitlements: {
151+
byok: true,
152+
inbox: true,
153+
customBlocks: true,
154+
forks: true,
155+
sandboxes: true,
156+
},
157+
}).map(({ id }) => id)
158+
).toContain('self-host')
159+
})
160+
161+
it('drops the Self-host section on hosted Sim', () => {
162+
setEnvFlags({ isHosted: true })
163+
164+
expect(buildUnifiedSettingsNavigation().map(({ id }) => id)).not.toContain('self-host')
165+
expect(
166+
resolveWorkspaceNavigation({
167+
permission: 'admin',
168+
permissionConfig: {},
169+
entitlements: {
170+
byok: true,
171+
inbox: true,
172+
customBlocks: true,
173+
forks: true,
174+
sandboxes: true,
175+
},
176+
}).map(({ id }) => id)
177+
).not.toContain('self-host')
178+
})
179+
130180
it('keeps the Sandboxes section on the pre-Daytona E2B flag alone', () => {
131181
setEnv({ NEXT_PUBLIC_SANDBOX_ENABLED: undefined, NEXT_PUBLIC_E2B_ENABLED: 'true' })
132182

@@ -367,6 +417,7 @@ describe('settings navigation boundaries', () => {
367417
'inbox',
368418
'recently-deleted',
369419
'custom-blocks',
420+
'self-host',
370421
],
371422
mutable: [],
372423
},
@@ -384,6 +435,7 @@ describe('settings navigation boundaries', () => {
384435
'inbox',
385436
'recently-deleted',
386437
'custom-blocks',
438+
'self-host',
387439
],
388440
mutable: ['secrets', 'custom-tools', 'mcp', 'workflow-mcp-servers', 'recently-deleted'],
389441
},
@@ -439,6 +491,7 @@ describe('settings navigation boundaries', () => {
439491
'recently-deleted',
440492
'forks',
441493
'custom-blocks',
494+
'self-host',
442495
])
443496
})
444497

apps/sim/components/settings/navigation.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export type WorkspaceSettingsSection =
7373
| 'recently-deleted'
7474
| 'forks'
7575
| 'custom-blocks'
76+
| 'self-host'
7677

7778
export type SettingsSection =
7879
| AccountSettingsSection
@@ -119,6 +120,7 @@ export type UnifiedSettingsSection =
119120
| 'data-drains'
120121
| 'mothership'
121122
| 'recently-deleted'
123+
| 'self-host'
122124

123125
export type UnifiedNavigationSection = 'account' | 'workspace' | 'organization' | 'platform'
124126

@@ -141,6 +143,12 @@ export interface UnifiedSettingsNavigationItem {
141143
requiresEnterprise?: boolean
142144
requiresMax?: boolean
143145
requiresHosted?: boolean
146+
/**
147+
* The inverse of {@link UnifiedSettingsNavigationItem.requiresHosted}: the
148+
* section exists only on a self-hosted deployment and is absent on Sim Cloud,
149+
* where the same surface is reached from the managed service instead.
150+
*/
151+
requiresSelfHosted?: boolean
144152
selfHostedOverride?: boolean
145153
requiresSuperUser?: boolean
146154
requiresAdminRole?: boolean
@@ -680,6 +688,20 @@ export const SETTINGS_SECTION_REGISTRY: readonly SettingsSectionRegistryEntry[]
680688
workspace: { id: 'recently-deleted', group: 'system', order: 9 },
681689
},
682690
},
691+
{
692+
label: 'Self-host',
693+
icon: Server,
694+
unified: {
695+
id: 'self-host',
696+
description: 'Manage this deployment from the Sim managed service.',
697+
group: 'workspace',
698+
order: 10,
699+
requiresSelfHosted: true,
700+
},
701+
planes: {
702+
workspace: { id: 'self-host', group: 'system', order: 12 },
703+
},
704+
},
683705
{
684706
label: 'Single sign-on',
685707
icon: LogIn,
@@ -821,6 +843,9 @@ export function buildUnifiedSettingsNavigation(): UnifiedSettingsNavigationItem[
821843
// `selfHostedOverride` short-circuit would otherwise reveal the tab on a
822844
// deployment that has the entitlement but no provider to run what it builds.
823845
if (unified.id === 'sandboxes' && !isSandboxExecutionAvailable()) return []
846+
// Dropped here so the sidebar, the route's `parseSection` gate, and section
847+
// metadata all agree that the section does not exist on Sim Cloud.
848+
if (unified.requiresSelfHosted && isHosted) return []
824849
const { group, ...item } = unified
825850
return [
826851
{
@@ -996,6 +1021,7 @@ const WORKSPACE_MUTATION_PERMISSION: Record<WorkspaceSettingsSection, Permission
9961021
'recently-deleted': 'write',
9971022
forks: 'admin',
9981023
'custom-blocks': 'admin',
1024+
'self-host': 'admin',
9991025
}
10001026

10011027
export interface WorkspaceMutationCapabilities {
@@ -1028,6 +1054,8 @@ export function resolveWorkspaceNavigation({
10281054
if (item.id === 'custom-blocks' && !entitlements.customBlocks) return []
10291055
// Removed, not locked: a missing provider is not something an upgrade fixes.
10301056
if (item.id === 'sandboxes' && !isSandboxExecutionAvailable()) return []
1057+
// Absent on Sim Cloud, where the managed service owns these settings.
1058+
if (item.id === 'self-host' && isHosted) return []
10311059

10321060
const lockedBy = LOCKABLE_WORKSPACE_SECTIONS[item.id]
10331061
const locked = lockedBy !== undefined && !entitlements[lockedBy]

0 commit comments

Comments
 (0)