Add shell registry and profile-driven app config support

This commit is contained in:
Amer Agovic
2026-05-10 20:48:13 -05:00
parent 8960e40db4
commit f5a739ee35
5 changed files with 75 additions and 15 deletions
+2
View File
@@ -84,6 +84,7 @@ export const CONFIG_KEYS = {
BRAND_LOGO: 'BRAND_LOGO', BRAND_LOGO: 'BRAND_LOGO',
THEME_COLOR: 'THEME_COLOR', THEME_COLOR: 'THEME_COLOR',
UI_SHELL: 'UI_SHELL', UI_SHELL: 'UI_SHELL',
INITIAL_ROUTE: 'INITIAL_ROUTE',
STORAGE_BACKEND: 'STORAGE_BACKEND', STORAGE_BACKEND: 'STORAGE_BACKEND',
API_BASE_URL: 'API_BASE_URL', API_BASE_URL: 'API_BASE_URL',
MODULES: 'MODULES', MODULES: 'MODULES',
@@ -152,6 +153,7 @@ export function initEnv(appConfig) {
BRAND_LOGO: appConfig.brand_logo || appConfig.brandLogo || appConfig.icons?.[0]?.src || '/favicon.svg', BRAND_LOGO: appConfig.brand_logo || appConfig.brandLogo || appConfig.icons?.[0]?.src || '/favicon.svg',
THEME_COLOR: appConfig.theme_color || appConfig.themeColor || '#000000', THEME_COLOR: appConfig.theme_color || appConfig.themeColor || '#000000',
UI_SHELL: appConfig.ui_shell || appConfig.uiShell || 'EmptyShell', UI_SHELL: appConfig.ui_shell || appConfig.uiShell || 'EmptyShell',
INITIAL_ROUTE: appConfig.initial_route || appConfig.initialRoute || appConfig.ui?.initial_route || appConfig.ui?.initialRoute || '/home',
STORAGE_BACKEND: appConfig.storage?.backend || 'localStorage', STORAGE_BACKEND: appConfig.storage?.backend || 'localStorage',
API_BASE_URL: resolvedApiBaseURL, API_BASE_URL: resolvedApiBaseURL,
MODULES: appConfig.modules || [], MODULES: appConfig.modules || [],
+22 -15
View File
@@ -6,7 +6,13 @@
import React, { createContext, useContext, useState, useEffect, useCallback, useRef, useMemo } from 'react'; import React, { createContext, useContext, useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { TamaguiProvider, Theme, createTamagui, YStack } from 'tamagui'; import { TamaguiProvider, Theme, createTamagui, YStack } from 'tamagui';
import { clearPWACache, getServiceWorkerStatus } from '../platform/sw-register.js'; import {
clearPWACache,
clearAllCaches,
clearAllStorage,
getServiceWorkerStatus,
unregisterAllServiceWorkers
} from '../platform/sw-register.js';
import { getProvider } from '../platform/storage.js'; import { getProvider } from '../platform/storage.js';
import * as apiClient from '../platform/api.js'; import * as apiClient from '../platform/api.js';
import * as storageModuleRef from '../platform/storage.js'; import * as storageModuleRef from '../platform/storage.js';
@@ -14,6 +20,7 @@ import * as menuRef from '../platform/menu.js';
import * as envModuleRef from '../platform/env.js'; import * as envModuleRef from '../platform/env.js';
import { getConfig, setConfig, CONFIG_KEYS, createLogger, startTrace, isDevelopment } from '../platform/env.js'; import { getConfig, setConfig, CONFIG_KEYS, createLogger, startTrace, isDevelopment } from '../platform/env.js';
import { EmptyShell, LandingShell, DashboardShell, AppInfo, Router } from './components/index.js'; import { EmptyShell, LandingShell, DashboardShell, AppInfo, Router } from './components/index.js';
import { resolveRegisteredShell } from './components/shell-registry.js';
import { LoginPage } from '../security/pages/LoginPage.jsx'; import { LoginPage } from '../security/pages/LoginPage.jsx';
import { getStyleTheme, DEFAULT_STYLE_THEME, normalizeStyleThemeName, setActiveStyleThemeName } from './styles/index.js'; import { getStyleTheme, DEFAULT_STYLE_THEME, normalizeStyleThemeName, setActiveStyleThemeName } from './styles/index.js';
import { THEME_MODE_CONFIG_KEY, THEME_NAME_CONFIG_KEY, THEME_MODES, themeManager } from './theme-controller.js'; import { THEME_MODE_CONFIG_KEY, THEME_NAME_CONFIG_KEY, THEME_MODES, themeManager } from './theme-controller.js';
@@ -48,8 +55,11 @@ function resolveShellComponent(shellName = 'EmptyShell') {
case 'dashboardshell': case 'dashboardshell':
return DashboardShell; return DashboardShell;
case 'emptyshell': case 'emptyshell':
default:
return EmptyShell; return EmptyShell;
default: {
const registeredShell = resolveRegisteredShell(shellName);
return registeredShell || EmptyShell;
}
} }
} }
@@ -84,6 +94,9 @@ function App({
const [menuItems, setMenuItems] = useState([]); const [menuItems, setMenuItems] = useState([]);
const [initialized, setInitialized] = useState(false); const [initialized, setInitialized] = useState(false);
const [ShellComponent, setShellComponent] = useState(() => resolveShellComponent(initialProfile?.ui_shell ?? 'EmptyShell')); const [ShellComponent, setShellComponent] = useState(() => resolveShellComponent(initialProfile?.ui_shell ?? 'EmptyShell'));
const [initialRoute, setInitialRoute] = useState(
initialProfile?.initial_route ?? initialProfile?.initialRoute ?? initialProfile?.ui?.initial_route ?? initialProfile?.ui?.initialRoute ?? '/home'
);
const [bootResult, setBootResult] = useState(null); const [bootResult, setBootResult] = useState(null);
const [bootModeOverride, setBootModeOverride] = useState(null); const [bootModeOverride, setBootModeOverride] = useState(null);
@@ -282,6 +295,9 @@ function App({
const Shell = resolveShellComponent(shellName); const Shell = resolveShellComponent(shellName);
setShellComponent(() => Shell); setShellComponent(() => Shell);
appLogger.log(`Using shell: ${shellName}`); appLogger.log(`Using shell: ${shellName}`);
const configuredInitialRoute = await services.env.getConfig(CONFIG_KEYS.INITIAL_ROUTE, '/home');
setInitialRoute(configuredInitialRoute || '/home');
// Get menu items from primary menu // Get menu items from primary menu
if (services.menu) { if (services.menu) {
@@ -405,7 +421,7 @@ function App({
appContent = <LoginPage />; appContent = <LoginPage />;
} else if (!shouldRenderBootScreen && !shouldHoldDuringInit) { } else if (!shouldRenderBootScreen && !shouldHoldDuringInit) {
appContent = ( appContent = (
<Router initialPath="/home"> <Router initialPath={initialRoute}>
{/* Declarative route registration (commented out - routes now registered programmatically via modules) {/* Declarative route registration (commented out - routes now registered programmatically via modules)
<Router.Endpoint path="/home" component={HomePage} /> <Router.Endpoint path="/home" component={HomePage} />
@@ -520,18 +536,9 @@ if (typeof window !== 'undefined') {
window.clearPWACache = clearPWACache; window.clearPWACache = clearPWACache;
window.__PWA_UTILS__ = { window.__PWA_UTILS__ = {
clearPWACache, clearPWACache,
clearAllCaches: async () => { clearAllCaches,
const { clearAllCaches } = await import('../platform/sw-register.js'); unregisterAllServiceWorkers,
return clearAllCaches(); clearAllStorage
},
unregisterAllServiceWorkers: async () => {
const { unregisterAllServiceWorkers } = await import('../platform/sw-register.js');
return unregisterAllServiceWorkers();
},
clearAllStorage: async () => {
const { clearAllStorage } = await import('../platform/sw-register.js');
return clearAllStorage();
}
}; };
console.log('💡 PWA Utilities available:'); console.log('💡 PWA Utilities available:');
+1
View File
@@ -394,6 +394,7 @@ const iconMap = {
'wifi': wrap(WifiHigh, 'WifiHigh'), 'wifi': wrap(WifiHigh, 'WifiHigh'),
'wifi-off': wrap(WifiSlash, 'WifiSlash'), 'wifi-off': wrap(WifiSlash, 'WifiSlash'),
'lightning': wrap(Lightning, 'Lightning'), 'lightning': wrap(Lightning, 'Lightning'),
'game': wrap(Lightning, 'Lightning'),
// ── Volume ───────────────────────────────────────────────────────────── // ── Volume ─────────────────────────────────────────────────────────────
'volume-up': wrap(SpeakerHigh, 'SpeakerHigh'), 'volume-up': wrap(SpeakerHigh, 'SpeakerHigh'),
+1
View File
@@ -25,6 +25,7 @@ export { Panel, default as PanelDefault } from './Panel.jsx';
export { SettingsPanel, default as SettingsPanelDefault } from './SettingsPanel.jsx'; export { SettingsPanel, default as SettingsPanelDefault } from './SettingsPanel.jsx';
export { GeneralConfig, default as GeneralConfigDefault } from './GeneralConfig.jsx'; export { GeneralConfig, default as GeneralConfigDefault } from './GeneralConfig.jsx';
export { IdentityConfig, default as IdentityConfigDefault } from './IdentityConfig.jsx'; export { IdentityConfig, default as IdentityConfigDefault } from './IdentityConfig.jsx';
export { registerShell, unregisterShell, resolveRegisteredShell, listRegisteredShells, clearRegisteredShells } from './shell-registry.js';
export * from './grid/index.js'; export * from './grid/index.js';
export { getTypographyRoleProps, getStyleTypography, TYPOGRAPHY_ROLE_KEYS } from '../styles/index.js'; export { getTypographyRoleProps, getStyleTypography, TYPOGRAPHY_ROLE_KEYS } from '../styles/index.js';
+49
View File
@@ -0,0 +1,49 @@
const SHELL_REGISTRY_KEY = '__bface_shell_registry__';
function getShellRegistry() {
const scope = typeof globalThis !== 'undefined' ? globalThis : window;
if (!scope[SHELL_REGISTRY_KEY]) {
scope[SHELL_REGISTRY_KEY] = new Map();
}
return scope[SHELL_REGISTRY_KEY];
}
function normalizeShellName(name = '') {
return String(name).trim().toLowerCase();
}
export function registerShell(name, component) {
const key = normalizeShellName(name);
if (!key || typeof component !== 'function') {
return false;
}
getShellRegistry().set(key, component);
return true;
}
export function unregisterShell(name) {
const key = normalizeShellName(name);
if (!key) {
return false;
}
return getShellRegistry().delete(key);
}
export function resolveRegisteredShell(name) {
const key = normalizeShellName(name);
if (!key) {
return null;
}
return getShellRegistry().get(key) || null;
}
export function listRegisteredShells() {
return Array.from(getShellRegistry().keys());
}
export function clearRegisteredShells() {
getShellRegistry().clear();
}