diff --git a/apps/ui-admin/src/App.vue b/apps/ui-admin/src/App.vue index 55130861e..8446871e1 100644 --- a/apps/ui-admin/src/App.vue +++ b/apps/ui-admin/src/App.vue @@ -6,13 +6,16 @@ import { computed, onMounted, shallowRef } from 'vue' import { RouterLink, RouterView, useRoute } from 'vue-router' import { Toaster } from 'vue-sonner' -import { adminApi, AdminApiError, signInUrl } from './modules/api' +import ApiEnvironmentSelect from './components/admin-shell/ApiEnvironmentSelect.vue' + +import { adminApi, AdminApiError, apiServerUrl, signInUrl } from './modules/api' const route = useRoute() const loading = shallowRef(true) const me = shallowRef(null) const accessError = shallowRef(null) +const currentApiServerUrl = apiServerUrl() const navItems = [ { to: '/', icon: 'i-lucide-layout-dashboard', label: 'Overview' }, @@ -138,6 +141,7 @@ onMounted(async () => { {{ currentTitle }} +
diff --git a/apps/ui-admin/src/components/admin-shell/ApiEnvironmentSelect.vue b/apps/ui-admin/src/components/admin-shell/ApiEnvironmentSelect.vue new file mode 100644 index 000000000..1ecca4c69 --- /dev/null +++ b/apps/ui-admin/src/components/admin-shell/ApiEnvironmentSelect.vue @@ -0,0 +1,60 @@ + + + diff --git a/apps/ui-admin/src/modules/server-admin-context.test.ts b/apps/ui-admin/src/modules/server-admin-context.test.ts index 8aa896cbf..94a78bd78 100644 --- a/apps/ui-admin/src/modules/server-admin-context.test.ts +++ b/apps/ui-admin/src/modules/server-admin-context.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' -import { defaultStandaloneApiServerUrl, getServerAdminBootstrapContext, resolveStandaloneServerAdminContext } from './server-admin-context' +import { ADMIN_API_ENVIRONMENTS, apiEnvironmentValueFor, buildApiServerSwitchUrl, defaultStandaloneApiServerUrl, getServerAdminBootstrapContext, resolveStandaloneServerAdminContext } from './server-admin-context' describe('ui-admin bootstrap context', () => { it('uses the trusted API server origin carried by standalone server redirects', () => { @@ -40,6 +40,25 @@ describe('ui-admin bootstrap context', () => { expect(defaultStandaloneApiServerUrl('https://server-dev.airi-server-admin.pages.dev')).toBe('https://airi-server-dev.up.railway.app') }) + it('exposes production, testing, and local API environment choices', () => { + expect(ADMIN_API_ENVIRONMENTS.map(environment => environment.value)).toEqual([ + 'https://api.airi.build', + 'https://airi-server-dev.up.railway.app', + 'http://localhost:3000', + ]) + }) + + it('maps custom localhost API origins to the local environment option', () => { + expect(apiEnvironmentValueFor('http://127.0.0.1:8787')).toBe('http://localhost:3000') + }) + + it('builds a switched admin URL without dropping the current page state', () => { + expect(buildApiServerSwitchUrl( + 'https://admin.airi.build/voice-packs/new?draft=1#advanced', + 'https://airi-server-dev.up.railway.app/api/admin', + )).toBe('https://admin.airi.build/voice-packs/new?draft=1&api_server_url=https%3A%2F%2Fairi-server-dev.up.railway.app#advanced') + }) + it('falls back to the standalone query context when the static placeholder script is still present', () => { document.body.innerHTML = '' window.history.replaceState( diff --git a/apps/ui-admin/src/modules/server-admin-context.ts b/apps/ui-admin/src/modules/server-admin-context.ts index fdc19781b..396835fa5 100644 --- a/apps/ui-admin/src/modules/server-admin-context.ts +++ b/apps/ui-admin/src/modules/server-admin-context.ts @@ -3,9 +3,33 @@ export interface ServerAdminBootstrapContext { currentUrl: string } +export interface AdminApiEnvironment { + label: string + value: string + description: string +} + const SCRIPT_ID = 'airi-server-admin-context' const API_SERVER_URL_QUERY_PARAM = 'api_server_url' +export const ADMIN_API_ENVIRONMENTS = [ + { + label: 'Production', + value: 'https://api.airi.build', + description: 'api.airi.build', + }, + { + label: 'Testing', + value: 'https://airi-server-dev.up.railway.app', + description: 'airi-server-dev.up.railway.app', + }, + { + label: 'Local', + value: 'http://localhost:3000', + description: 'localhost:3000', + }, +] as const satisfies readonly AdminApiEnvironment[] + const TRUSTED_STANDALONE_API_SERVER_ORIGINS = [ 'https://api.airi.build', 'https://airi-server-dev.up.railway.app', @@ -107,6 +131,25 @@ export function resolveStandaloneServerAdminContext(currentUrl: string): ServerA } } +export function apiEnvironmentValueFor(apiServerUrl: string): string { + const origin = new URL(apiServerUrl).origin + const known = ADMIN_API_ENVIRONMENTS.find(environment => environment.value === origin) + if (known) + return known.value + + if (TRUSTED_LOCAL_API_SERVER_ORIGIN_PATTERNS.some(pattern => pattern.test(origin))) + return ADMIN_API_ENVIRONMENTS.find(environment => environment.label === 'Local')?.value ?? origin + + return origin +} + +export function buildApiServerSwitchUrl(currentUrl: string, apiServerUrl: string): string { + const url = new URL(currentUrl) + const origin = new URL(apiServerUrl).origin + url.searchParams.set(API_SERVER_URL_QUERY_PARAM, origin) + return url.toString() +} + function normalizeTrustedApiServerUrl(value: string | null): string | null { if (!value) return null