diff --git a/frontend/src/v2/__tests__/V2SettingsPage.test.tsx b/frontend/src/v2/__tests__/V2SettingsPage.test.tsx
index 73e73a97f5..04be03b7e0 100644
--- a/frontend/src/v2/__tests__/V2SettingsPage.test.tsx
+++ b/frontend/src/v2/__tests__/V2SettingsPage.test.tsx
@@ -138,14 +138,13 @@ describe('V2SettingsPage', () => {
expect(screen.getByText('Device controls')).toBeInTheDocument();
expect(screen.getByText('Plan controls')).toHaveAttribute('data-show-heading', 'false');
expect(screen.getByText('Device controls')).toHaveAttribute('data-show-heading', 'false');
- expect(screen.getByText('Connected app controls')).toBeInTheDocument();
+ // Developer apps stay hidden unless REACT_APP_DEVELOPER_APPS=true (TASK-674).
+ expect(screen.queryByText('Connected app controls')).not.toBeInTheDocument();
expect(screen.getByRole('img', { name: 'lily avatar' })).toHaveClass('v2-settings__avatar');
expect(screen.getByRole('img', { name: 'lily avatar' })).toHaveAttribute('src', '/uploads/lily.png');
expect(screen.getByRole('img', { name: 'lily avatar' })).toHaveAttribute('data-kind', 'human');
- expect(screen.getByText('Connected app controls')).toHaveAttribute('data-variant', 'settings');
- expect(screen.getByText('Connected app controls')).toHaveAttribute('data-show-integrations', 'false');
expect(screen.getByRole('link', { name: /Connectors/ })).toHaveAttribute('href', '/v2/connectors');
- expect(screen.getByText('Developer apps').closest('details')).not.toHaveAttribute('open');
+ expect(screen.queryByText('Developer apps')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Regenerate' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Upload photo' })).toBeInTheDocument();
expect(screen.getByText('Regenerate for another, or upload a photo.')).toBeInTheDocument();
@@ -153,6 +152,24 @@ describe('V2SettingsPage', () => {
expect(screen.getByRole('radio', { name: '中文' })).toBeInTheDocument();
});
+ test('shows Developer apps only when REACT_APP_DEVELOPER_APPS is exactly true', () => {
+ (axios.get as jest.Mock).mockResolvedValue({ data: { hasToken: false } });
+ const previous = process.env.REACT_APP_DEVELOPER_APPS;
+ try {
+ process.env.REACT_APP_DEVELOPER_APPS = 'yes';
+ const off = renderSettings();
+ expect(screen.queryByText('Developer apps')).not.toBeInTheDocument();
+ off.unmount();
+
+ process.env.REACT_APP_DEVELOPER_APPS = 'true';
+ renderSettings();
+ expect(screen.getByText('Developer apps')).toBeInTheDocument();
+ } finally {
+ if (previous === undefined) delete process.env.REACT_APP_DEVELOPER_APPS;
+ else process.env.REACT_APP_DEVELOPER_APPS = previous;
+ }
+ });
+
test('uses a left navigation column to target Settings sections', () => {
(axios.get as jest.Mock).mockResolvedValue({ data: { hasToken: false } });
renderSettings();
diff --git a/frontend/src/v2/components/V2SettingsPage.tsx b/frontend/src/v2/components/V2SettingsPage.tsx
index 9e4c8f2c0d..327a8e8e8f 100644
--- a/frontend/src/v2/components/V2SettingsPage.tsx
+++ b/frontend/src/v2/components/V2SettingsPage.tsx
@@ -42,6 +42,10 @@ const SETTINGS_SECTIONS = [
{ id: 'language', title: 'Language' },
] as const;
+// Custom "Developer apps" (AppsManagement) stays hidden until every path
+// (create, edit, delete, use) is walked live. Default off (TASK-674, D215).
+const developerAppsEnabled = () => process.env.REACT_APP_DEVELOPER_APPS === 'true';
+
// Admin-only: the instance's user list and usage analytics. Their routes
// survived the Settings consolidation (#1544) but the way in did not — the
// pages were reachable only by typing the URL (2026-09-08).
@@ -554,10 +558,12 @@ const V2SettingsPage: React.FC = () => {
label="Connectors"
detail="Manage connected services and channels"
/>
-
- Developer apps
-
-
+ {developerAppsEnabled() && (
+
+ Developer apps
+
+
+ )}
{isAdmin && (
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index 1153c173c8..87debaadc7 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -41,6 +41,8 @@ export default defineConfig({
// Error tracking is opt-in at build time. An empty value lets Rollup remove
// the guarded dynamic import, keeping the SDK out of self-hosted bundles.
'process.env.REACT_APP_SENTRY_DSN': JSON.stringify(process.env.REACT_APP_SENTRY_DSN || ''),
+ // Developer apps flag (TASK-674). Unset in CI and deploy, so prod stays off.
+ 'process.env.REACT_APP_DEVELOPER_APPS': JSON.stringify(process.env.REACT_APP_DEVELOPER_APPS || ''),
'process.env': {},
},
});