mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
🐛 fix(calendar): use runtime config for cross-app service URLs
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 <noreply@anthropic.com>
This commit is contained in:
parent
7b33f6a761
commit
d2c2326ed7
8 changed files with 218 additions and 24 deletions
|
|
@ -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 <<EOF
|
||||
{
|
||||
"BACKEND_URL": "${BACKEND_URL}",
|
||||
"AUTH_URL": "${AUTH_URL}"
|
||||
"AUTH_URL": "${AUTH_URL}",
|
||||
"TODO_API_URL": "${TODO_API_URL}",
|
||||
"CONTACTS_API_URL": "${CONTACTS_API_URL}"
|
||||
}
|
||||
EOF
|
||||
|
||||
|
|
|
|||
|
|
@ -3,15 +3,22 @@
|
|||
* Allows Calendar app to fetch contact birthdays for display
|
||||
*/
|
||||
|
||||
import { env } from '$env/dynamic/public';
|
||||
import { createApiClient } from './base-client';
|
||||
import { getContactsApiUrl } from '$lib/config/runtime';
|
||||
|
||||
const CONTACTS_API_BASE = env.PUBLIC_CONTACTS_API_URL || 'http://localhost:3015';
|
||||
// Lazy-initialized client (runtime config is async)
|
||||
let contactsClient: ReturnType<typeof createApiClient> | 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<T>(
|
||||
endpoint: string,
|
||||
options?: Parameters<ReturnType<typeof createApiClient>['fetchApi']>[1]
|
||||
) {
|
||||
const client = await getContactsClient();
|
||||
return client.fetchApi<T>(endpoint, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all contacts with birthdays from Contacts service
|
||||
|
|
|
|||
|
|
@ -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<typeof createApiClient> | 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<T>(
|
||||
endpoint: string,
|
||||
options?: Parameters<ReturnType<typeof createApiClient>['fetchApi']>[1]
|
||||
) {
|
||||
const client = await getTodoClient();
|
||||
return client.fetchApi<T>(endpoint, options);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Task API Functions
|
||||
|
|
|
|||
|
|
@ -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<string> {
|
|||
return config.BACKEND_URL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get todo service URL
|
||||
*/
|
||||
export async function getTodoApiUrl(): Promise<string> {
|
||||
const config = await loadConfig();
|
||||
return config.TODO_API_URL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contacts service URL
|
||||
*/
|
||||
export async function getContactsApiUrl(): Promise<string> {
|
||||
const config = await loadConfig();
|
||||
return config.CONTACTS_API_URL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full runtime config
|
||||
*/
|
||||
export async function getConfig(): Promise<RuntimeConfig> {
|
||||
return loadConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize configuration (call early in app lifecycle)
|
||||
* This triggers the config load and caches it for subsequent calls
|
||||
|
|
|
|||
|
|
@ -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<typeof createUserSettingsStore> | 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<ReturnType<typeof createUserSettingsStore>['updateGlobal']>[0]
|
||||
) {
|
||||
const store = await ensureStore();
|
||||
return store.updateGlobal(settings);
|
||||
},
|
||||
async updateAppOverride(
|
||||
settings: Parameters<ReturnType<typeof createUserSettingsStore>['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<ReturnType<typeof createUserSettingsStore>['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<string, unknown>) {
|
||||
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);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue