From d2c2326ed722dd88b7c4f428b4d30aadae84937d Mon Sep 17 00:00:00 2001 From: Wuesteon Date: Tue, 16 Dec 2025 05:10:01 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(calendar):=20use=20runtime?= =?UTF-8?q?=20config=20for=20cross-app=20service=20URLs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calendar app was using $env/dynamic/public for TODO and Contacts API URLs, which doesn't work in SPA mode. Updated to use runtime config: - Add TODO_API_URL and CONTACTS_API_URL to runtime config - Update docker-entrypoint.sh to include new env vars - Refactor todos.ts and birthdays.ts to use lazy-loaded clients - Refactor user-settings.svelte.ts with lazy store initialization - Add env vars to docker-compose.staging.yml This fixes staging deployment where cross-app integrations were calling localhost instead of staging URLs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/calendar/apps/web/docker-entrypoint.sh | 8 +- .../apps/web/src/lib/api/birthdays.ts | 27 +++- apps/calendar/apps/web/src/lib/api/todos.ts | 27 +++- .../apps/web/src/lib/config/runtime.ts | 29 ++++ .../src/lib/stores/user-settings.svelte.ts | 137 +++++++++++++++++- .../apps/web/src/routes/+layout.svelte | 8 +- apps/calendar/apps/web/static/config.json | 4 +- docker-compose.staging.yml | 2 + 8 files changed, 218 insertions(+), 24 deletions(-) diff --git a/apps/calendar/apps/web/docker-entrypoint.sh b/apps/calendar/apps/web/docker-entrypoint.sh index df8e2168d..32d58c329 100644 --- a/apps/calendar/apps/web/docker-entrypoint.sh +++ b/apps/calendar/apps/web/docker-entrypoint.sh @@ -6,16 +6,22 @@ echo "🔧 Generating runtime configuration..." # Environment variables with development defaults BACKEND_URL=${BACKEND_URL:-"http://localhost:3016"} AUTH_URL=${AUTH_URL:-"http://localhost:3001"} +TODO_API_URL=${TODO_API_URL:-"http://localhost:3018"} +CONTACTS_API_URL=${CONTACTS_API_URL:-"http://localhost:3015"} echo "📝 Config values:" echo " BACKEND_URL: $BACKEND_URL" echo " AUTH_URL: $AUTH_URL" +echo " TODO_API_URL: $TODO_API_URL" +echo " CONTACTS_API_URL: $CONTACTS_API_URL" # Generate config.json from environment variables cat > /app/apps/calendar/apps/web/build/client/config.json < | null = null; -const contactsClient = createApiClient({ - baseUrl: CONTACTS_API_BASE, - apiPrefix: '/api/v1', -}); +async function getContactsClient() { + if (!contactsClient) { + const contactsApiUrl = await getContactsApiUrl(); + contactsClient = createApiClient({ + baseUrl: contactsApiUrl, + apiPrefix: '/api/v1', + }); + } + return contactsClient; +} // ============================================ // Types for Birthday Integration @@ -61,7 +68,13 @@ interface BirthdaysResponse { // API Functions // ============================================ -const fetchContactsApi = contactsClient.fetchApi; +async function fetchContactsApi( + endpoint: string, + options?: Parameters['fetchApi']>[1] +) { + const client = await getContactsClient(); + return client.fetchApi(endpoint, options); +} /** * Fetch all contacts with birthdays from Contacts service diff --git a/apps/calendar/apps/web/src/lib/api/todos.ts b/apps/calendar/apps/web/src/lib/api/todos.ts index db72aac5d..2737d95df 100644 --- a/apps/calendar/apps/web/src/lib/api/todos.ts +++ b/apps/calendar/apps/web/src/lib/api/todos.ts @@ -3,15 +3,22 @@ * Allows Calendar app to fetch/manage todos from the Todo service */ -import { env } from '$env/dynamic/public'; import { createApiClient, buildQueryString } from './base-client'; +import { getTodoApiUrl } from '$lib/config/runtime'; -const TODO_API_BASE = env.PUBLIC_TODO_BACKEND_URL || 'http://localhost:3018'; +// Lazy-initialized client (runtime config is async) +let todoClient: ReturnType | null = null; -const todoClient = createApiClient({ - baseUrl: TODO_API_BASE, - apiPrefix: '/api/v1', -}); +async function getTodoClient() { + if (!todoClient) { + const todoApiUrl = await getTodoApiUrl(); + todoClient = createApiClient({ + baseUrl: todoApiUrl, + apiPrefix: '/api/v1', + }); + } + return todoClient; +} // ============================================ // Types (mirrored from @todo/shared for cross-app use) @@ -173,7 +180,13 @@ interface LabelsResponse { // API Client (using shared base client) // ============================================ -const fetchTodoApi = todoClient.fetchApi; +async function fetchTodoApi( + endpoint: string, + options?: Parameters['fetchApi']>[1] +) { + const client = await getTodoClient(); + return client.fetchApi(endpoint, options); +} // ============================================ // Task API Functions diff --git a/apps/calendar/apps/web/src/lib/config/runtime.ts b/apps/calendar/apps/web/src/lib/config/runtime.ts index dcc567d73..2163c83b7 100644 --- a/apps/calendar/apps/web/src/lib/config/runtime.ts +++ b/apps/calendar/apps/web/src/lib/config/runtime.ts @@ -13,6 +13,8 @@ import { z } from 'zod'; export interface RuntimeConfig { BACKEND_URL: string; AUTH_URL: string; + TODO_API_URL: string; + CONTACTS_API_URL: string; } /** @@ -22,6 +24,8 @@ export interface RuntimeConfig { const ConfigSchema = z.object({ BACKEND_URL: z.string().url().min(1, 'BACKEND_URL must be a valid URL'), AUTH_URL: z.string().url().min(1, 'AUTH_URL must be a valid URL'), + TODO_API_URL: z.string().url().min(1, 'TODO_API_URL must be a valid URL'), + CONTACTS_API_URL: z.string().url().min(1, 'CONTACTS_API_URL must be a valid URL'), }); /** @@ -34,6 +38,8 @@ const ConfigSchema = z.object({ const DEV_CONFIG: RuntimeConfig = { BACKEND_URL: 'http://localhost:3016', AUTH_URL: 'http://localhost:3001', + TODO_API_URL: 'http://localhost:3018', + CONTACTS_API_URL: 'http://localhost:3015', }; let cachedConfig: RuntimeConfig | null = null; @@ -107,6 +113,29 @@ export async function getBackendUrl(): Promise { return config.BACKEND_URL; } +/** + * Get todo service URL + */ +export async function getTodoApiUrl(): Promise { + const config = await loadConfig(); + return config.TODO_API_URL; +} + +/** + * Get contacts service URL + */ +export async function getContactsApiUrl(): Promise { + const config = await loadConfig(); + return config.CONTACTS_API_URL; +} + +/** + * Get full runtime config + */ +export async function getConfig(): Promise { + return loadConfig(); +} + /** * Initialize configuration (call early in app lifecycle) * This triggers the config load and caches it for subsequent calls diff --git a/apps/calendar/apps/web/src/lib/stores/user-settings.svelte.ts b/apps/calendar/apps/web/src/lib/stores/user-settings.svelte.ts index a2e5e08b7..dace27217 100644 --- a/apps/calendar/apps/web/src/lib/stores/user-settings.svelte.ts +++ b/apps/calendar/apps/web/src/lib/stores/user-settings.svelte.ts @@ -5,14 +5,17 @@ * - Global settings that apply to all apps * - Per-app overrides for customization * - localStorage caching for offline support + * + * Uses lazy initialization to wait for runtime config to load. */ import { browser } from '$app/environment'; import { createUserSettingsStore } from '@manacore/shared-theme'; import { authStore } from './auth.svelte'; +import { getAuthUrl as getRuntimeAuthUrl } from '$lib/config/runtime'; -// Get auth URL dynamically at runtime -function getAuthUrl(): string { +// Get auth URL with fallback for early access (before runtime config loads) +function getAuthUrlSync(): string { if (browser && typeof window !== 'undefined') { const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string }) .__PUBLIC_MANA_CORE_AUTH_URL__; @@ -21,8 +24,128 @@ function getAuthUrl(): string { return 'http://localhost:3001'; } -export const userSettings = createUserSettingsStore({ - appId: 'calendar', - authUrl: getAuthUrl(), - getAccessToken: () => authStore.getAccessToken(), -}); +// Lazy-initialized store (created after runtime config is loaded) +let _store: ReturnType | null = null; + +function getOrCreateStore(authUrl?: string) { + if (!_store) { + _store = createUserSettingsStore({ + appId: 'calendar', + authUrl: authUrl || getAuthUrlSync(), + getAccessToken: () => authStore.getAccessToken(), + }); + } + return _store; +} + +// Ensure store is initialized with correct URL from runtime config +async function ensureStore() { + if (!_store) { + const authUrl = await getRuntimeAuthUrl(); + _store = createUserSettingsStore({ + appId: 'calendar', + authUrl, + getAccessToken: () => authStore.getAccessToken(), + }); + } + return _store; +} + +// Export proxy that lazily initializes the store +export const userSettings = { + // Getters - use sync store (may have fallback URL initially) + get nav() { + return getOrCreateStore().nav; + }, + get theme() { + return getOrCreateStore().theme; + }, + get locale() { + return getOrCreateStore().locale; + }, + get general() { + return getOrCreateStore().general; + }, + get startPage() { + return getOrCreateStore().startPage; + }, + get globalSettings() { + return getOrCreateStore().globalSettings; + }, + get hasAppOverride() { + return getOrCreateStore().hasAppOverride; + }, + get syncing() { + return getOrCreateStore().syncing; + }, + get loaded() { + return getOrCreateStore().loaded; + }, + get deviceId() { + return getOrCreateStore().deviceId; + }, + get deviceSettings() { + return getOrCreateStore().deviceSettings; + }, + get currentDeviceAppSettings() { + return getOrCreateStore().currentDeviceAppSettings; + }, + + // Methods that make API calls - ensure store has correct URL + async load() { + const store = await ensureStore(); + return store.load(); + }, + async updateGlobal( + settings: Parameters['updateGlobal']>[0] + ) { + const store = await ensureStore(); + return store.updateGlobal(settings); + }, + async updateAppOverride( + settings: Parameters['updateAppOverride']>[0] + ) { + const store = await ensureStore(); + return store.updateAppOverride(settings); + }, + async removeAppOverride() { + const store = await ensureStore(); + return store.removeAppOverride(); + }, + async setStartPage(appId: string, path: string) { + const store = await ensureStore(); + return store.setStartPage(appId, path); + }, + async updateGeneral( + settings: Parameters['updateGeneral']>[0] + ) { + const store = await ensureStore(); + return store.updateGeneral(settings); + }, + getHiddenNavItemsForApp(appId: string) { + return getOrCreateStore().getHiddenNavItemsForApp(appId); + }, + async toggleNavItemVisibility(appId: string, href: string) { + const store = await ensureStore(); + return store.toggleNavItemVisibility(appId, href); + }, + async setHiddenNavItems(appId: string, hiddenHrefs: string[]) { + const store = await ensureStore(); + return store.setHiddenNavItems(appId, hiddenHrefs); + }, + async updateDeviceAppSettings(settings: Record) { + const store = await ensureStore(); + return store.updateDeviceAppSettings(settings); + }, + getDeviceAppSettings() { + return getOrCreateStore().getDeviceAppSettings(); + }, + async getDevices() { + const store = await ensureStore(); + return store.getDevices(); + }, + async removeDevice(deviceId: string) { + const store = await ensureStore(); + return store.removeDevice(deviceId); + }, +}; diff --git a/apps/calendar/apps/web/src/routes/+layout.svelte b/apps/calendar/apps/web/src/routes/+layout.svelte index f2fe98378..7328202b8 100644 --- a/apps/calendar/apps/web/src/routes/+layout.svelte +++ b/apps/calendar/apps/web/src/routes/+layout.svelte @@ -16,9 +16,15 @@ onMount(async () => { // Initialize runtime config first (12-factor pattern) - const { initializeConfig } = await import('$lib/config/runtime'); + const { initializeConfig, getConfig } = await import('$lib/config/runtime'); await initializeConfig(); + // Inject config into window for stores that need synchronous access + const config = await getConfig(); + ( + window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string } + ).__PUBLIC_MANA_CORE_AUTH_URL__ = config.AUTH_URL; + // Wait for i18n locale to be loaded await waitLocale(); diff --git a/apps/calendar/apps/web/static/config.json b/apps/calendar/apps/web/static/config.json index 8e9155ff9..7f993ac39 100644 --- a/apps/calendar/apps/web/static/config.json +++ b/apps/calendar/apps/web/static/config.json @@ -1,4 +1,6 @@ { "BACKEND_URL": "http://localhost:3016", - "AUTH_URL": "http://localhost:3001" + "AUTH_URL": "http://localhost:3001", + "TODO_API_URL": "http://localhost:3018", + "CONTACTS_API_URL": "http://localhost:3015" } diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml index 6fe02e7d4..2af8e9bb6 100644 --- a/docker-compose.staging.yml +++ b/docker-compose.staging.yml @@ -319,6 +319,8 @@ services: # These vars are used by docker-entrypoint.sh to generate /config.json BACKEND_URL: https://calendar-api.staging.manacore.ai AUTH_URL: https://auth.staging.manacore.ai + TODO_API_URL: https://todo-api.staging.manacore.ai + CONTACTS_API_URL: https://contacts-api.staging.manacore.ai ports: - "5186:5186" healthcheck: