diff --git a/backend/alwrity_utils/feature_profiles.py b/backend/alwrity_utils/feature_profiles.py index c13636c8..59fb7fdb 100644 --- a/backend/alwrity_utils/feature_profiles.py +++ b/backend/alwrity_utils/feature_profiles.py @@ -9,7 +9,9 @@ from typing import Iterable, Tuple from .feature_registry import FEATURE_GROUPS, PROFILE_GROUP_MAP -ENV_FEATURE_PROFILE = "ALWRITY_FEATURE_TO_ENABLE" +# Consolidated env var - supports both old and new format +ENV_FEATURE_PROFILE = "ALWRITY_ENABLED_FEATURES" +ENV_FEATURE_PROFILE_LEGACY = "ALWRITY_FEATURE_TO_ENABLE" DEFAULT_PROFILE = "all" @@ -22,7 +24,12 @@ class ExpandedFeatureProfile: class UnknownFeatureProfileError(ValueError): - """Raised when ALWRITY_FEATURE_TO_ENABLE contains unknown profile values.""" + """Raised when ALWRITY_ENABLED_FEATURES contains unknown profile values.""" + + +def _get_env_value() -> str: + """Get the feature profile value from environment - new var takes precedence.""" + return os.getenv(ENV_FEATURE_PROFILE) or os.getenv(ENV_FEATURE_PROFILE_LEGACY) or DEFAULT_PROFILE def _normalize_values(raw_value: str | None) -> Tuple[str, ...]: @@ -44,11 +51,11 @@ def parse_feature_profiles(raw_value: str | None = None) -> Tuple[str, ...]: Raises UnknownFeatureProfileError when any profile is not registered. """ - selected_profiles = _normalize_values(raw_value if raw_value is not None else os.getenv(ENV_FEATURE_PROFILE)) + selected_profiles = _normalize_values(raw_value if raw_value is not None else _get_env_value()) - unknown = sorted({profile for profile in selected_profiles if profile not in PROFILE_GROUP_MAP}) + unknown = sorted({profile for profile in selected_profiles if profile not in PROFILE_GROUP_MAP and profile not in FEATURE_GROUPS}) if unknown: - supported = ", ".join(sorted(PROFILE_GROUP_MAP)) + supported = ", ".join(sorted(set(PROFILE_GROUP_MAP.keys()) | set(FEATURE_GROUPS.keys()))) unknown_display = ", ".join(unknown) raise UnknownFeatureProfileError( f"Unknown {ENV_FEATURE_PROFILE} value(s): {unknown_display}. Supported profiles: {supported}." @@ -64,14 +71,18 @@ def _dedupe_stable(items: Iterable[str]) -> Tuple[str, ...]: def expand_profiles(profiles: Tuple[str, ...]) -> ExpandedFeatureProfile: """Expand profile names into a deduplicated group list.""" + # Handle "all" specially - include all groups + if "all" in profiles: + return ExpandedFeatureProfile(profiles=("all",), groups=tuple(FEATURE_GROUPS.keys())) + + # Otherwise expand via PROFILE_GROUP_MAP groups = _dedupe_stable( group for profile in profiles - for group in PROFILE_GROUP_MAP[profile] + for group in PROFILE_GROUP_MAP.get(profile, (profile,)) ) - missing_groups = sorted({group for group in groups if group not in FEATURE_GROUPS}) - if missing_groups: - raise RuntimeError(f"Profile mapping references unknown groups: {', '.join(missing_groups)}") + # Include FEATURE_GROUPS keys directly + all_groups = _dedupe_stable(list(groups) + [g for g in groups if g in FEATURE_GROUPS]) - return ExpandedFeatureProfile(profiles=profiles, groups=groups) + return ExpandedFeatureProfile(profiles=profiles, groups=all_groups) diff --git a/backend/alwrity_utils/router_manager.py b/backend/alwrity_utils/router_manager.py index caabc4d9..eee799ce 100644 --- a/backend/alwrity_utils/router_manager.py +++ b/backend/alwrity_utils/router_manager.py @@ -13,57 +13,57 @@ from loguru import logger CORE_ROUTER_REGISTRY = [ - {"name": "component_logic", "module": "api.component_logic", "attr": "router", "profiles": {"all", "default"}}, - {"name": "subscription", "module": "api.subscription", "attr": "router", "profiles": {"all", "default", "podcast"}}, - {"name": "step3_research", "module": "api.onboarding_utils.step3_routes", "attr": "router", "profiles": {"all", "default"}}, - {"name": "step4_assets", "module": "api.onboarding_utils.step4_asset_routes", "attr": "router", "profiles": {"all", "default"}}, - {"name": "step4_persona", "module": "api.onboarding_utils.step4_persona_routes_optimized", "attr": "router", "profiles": {"all", "default"}}, - {"name": "gsc_auth", "module": "routers.gsc_auth", "attr": "router", "profiles": {"all", "default"}}, - {"name": "wordpress_oauth", "module": "routers.wordpress_oauth", "attr": "router", "profiles": {"all", "default"}}, - {"name": "bing_oauth", "module": "routers.bing_oauth", "attr": "router", "profiles": {"all", "default"}}, - {"name": "bing_analytics", "module": "routers.bing_analytics", "attr": "router", "profiles": {"all", "default"}}, - {"name": "bing_analytics_storage", "module": "routers.bing_analytics_storage", "attr": "router", "profiles": {"all", "default"}}, - {"name": "seo_tools", "module": "routers.seo_tools", "attr": "router", "profiles": {"all", "default"}}, - {"name": "facebook_writer", "module": "api.facebook_writer.routers", "attr": "facebook_router", "profiles": {"all", "default"}}, - {"name": "linkedin", "module": "routers.linkedin", "attr": "router", "profiles": {"all", "default"}}, - {"name": "linkedin_image", "module": "api.linkedin_image_generation", "attr": "router", "profiles": {"all", "default"}}, - {"name": "brainstorm", "module": "api.brainstorm", "attr": "router", "profiles": {"all", "default"}}, - {"name": "hallucination_detector", "module": "api.hallucination_detector", "attr": "router", "profiles": {"all", "default"}}, - {"name": "writing_assistant", "module": "api.writing_assistant", "attr": "router", "profiles": {"all", "default"}}, - {"name": "content_planning", "module": "api.content_planning.api.router", "attr": "router", "profiles": {"all", "default"}}, - {"name": "user_data", "module": "api.user_data", "attr": "router", "profiles": {"all", "default"}}, - {"name": "user_environment", "module": "api.user_environment", "attr": "router", "profiles": {"all", "default"}}, - {"name": "strategy_copilot", "module": "api.content_planning.strategy_copilot", "attr": "router", "profiles": {"all", "default"}}, - {"name": "error_logging", "module": "routers.error_logging", "attr": "router", "profiles": {"all", "default"}}, - {"name": "frontend_env_manager", "module": "routers.frontend_env_manager", "attr": "router", "profiles": {"all", "default"}}, - {"name": "platform_analytics", "module": "routers.platform_analytics", "attr": "router", "profiles": {"all", "default"}}, - {"name": "bing_insights", "module": "routers.bing_insights", "attr": "router", "profiles": {"all", "default"}}, - {"name": "background_jobs", "module": "routers.background_jobs", "attr": "router", "profiles": {"all", "default"}}, + {"name": "component_logic", "module": "api.component_logic", "attr": "router", "features": {"all", "core"}}, + {"name": "subscription", "module": "api.subscription", "attr": "router", "features": {"all", "core", "podcast", "blog-writer", "youtube"}}, + {"name": "step3_research", "module": "api.onboarding_utils.step3_routes", "attr": "router", "features": {"all", "core"}}, + {"name": "step4_assets", "module": "api.onboarding_utils.step4_asset_routes", "attr": "router", "features": {"all", "core"}}, + {"name": "step4_persona", "module": "api.onboarding_utils.step4_persona_routes_optimized", "attr": "router", "features": {"all", "core"}}, + {"name": "gsc_auth", "module": "routers.gsc_auth", "attr": "router", "features": {"all", "core", "seo"}}, + {"name": "wordpress_oauth", "module": "routers.wordpress_oauth", "attr": "router", "features": {"all", "core"}}, + {"name": "bing_oauth", "module": "routers.bing_oauth", "attr": "router", "features": {"all", "core"}}, + {"name": "bing_analytics", "module": "routers.bing_analytics", "attr": "router", "features": {"all", "core"}}, + {"name": "bing_analytics_storage", "module": "routers.bing_analytics_storage", "attr": "router", "features": {"all", "core"}}, + {"name": "seo_tools", "module": "routers.seo_tools", "attr": "router", "features": {"all", "core", "seo"}}, + {"name": "facebook_writer", "module": "api.facebook_writer.routers", "attr": "facebook_router", "features": {"all", "core", "facebook"}}, + {"name": "linkedin", "module": "routers.linkedin", "attr": "router", "features": {"all", "core", "linkedin"}}, + {"name": "linkedin_image", "module": "api.linkedin_image_generation", "attr": "router", "features": {"all", "core", "linkedin"}}, + {"name": "brainstorm", "module": "api.brainstorm", "attr": "router", "features": {"all", "core"}}, + {"name": "hallucination_detector", "module": "api.hallucination_detector", "attr": "router", "features": {"all", "core"}}, + {"name": "writing_assistant", "module": "api.writing_assistant", "attr": "router", "features": {"all", "core"}}, + {"name": "content_planning", "module": "api.content_planning.api.router", "attr": "router", "features": {"all", "core", "content-planning"}}, + {"name": "user_data", "module": "api.user_data", "attr": "router", "features": {"all", "core"}}, + {"name": "user_environment", "module": "api.user_environment", "attr": "router", "features": {"all", "core"}}, + {"name": "strategy_copilot", "module": "api.content_planning.strategy_copilot", "attr": "router", "features": {"all", "core", "content-planning"}}, + {"name": "error_logging", "module": "routers.error_logging", "attr": "router", "features": {"all", "core"}}, + {"name": "frontend_env_manager", "module": "routers.frontend_env_manager", "attr": "router", "features": {"all", "core"}}, + {"name": "platform_analytics", "module": "routers.platform_analytics", "attr": "router", "features": {"all", "core"}}, + {"name": "bing_insights", "module": "routers.bing_insights", "attr": "router", "features": {"all", "core", "seo"}}, + {"name": "background_jobs", "module": "routers.background_jobs", "attr": "router", "features": {"all", "core"}}, ] OPTIONAL_ROUTER_REGISTRY = [ - {"name": "blog_writer", "module": "api.blog_writer.router", "attr": "router", "profiles": {"all", "default"}}, - {"name": "story_writer", "module": "api.story_writer.router", "attr": "router", "profiles": {"all", "default"}}, - {"name": "wix", "module": "api.wix_routes", "attr": "router", "profiles": {"all", "default"}}, - {"name": "blog_seo_analysis", "module": "api.blog_writer.seo_analysis", "attr": "router", "profiles": {"all", "default"}}, - {"name": "persona", "module": "api.persona_routes", "attr": "router", "profiles": {"all", "default"}}, - {"name": "video_studio", "module": "api.video_studio.router", "attr": "router", "profiles": {"all", "default"}}, - {"name": "stability", "module": "routers.stability", "attr": "router", "profiles": {"all", "default"}}, - {"name": "stability_advanced", "module": "routers.stability_advanced", "attr": "router", "profiles": {"all", "default"}}, - {"name": "stability_admin", "module": "routers.stability_admin", "attr": "router", "profiles": {"all", "default"}}, - {"name": "images", "module": "api.images", "attr": "router", "profiles": {"all", "default"}}, - {"name": "image_studio", "module": "routers.image_studio", "attr": "router", "profiles": {"all", "default"}}, - {"name": "product_marketing", "module": "routers.product_marketing", "attr": "router", "profiles": {"all", "default"}}, - {"name": "campaign_creator", "module": "routers.campaign_creator", "attr": "router", "profiles": {"all", "default"}}, - {"name": "content_assets", "module": "api.content_assets.router", "attr": "router", "profiles": {"all", "default"}}, - {"name": "podcast", "module": "api.podcast.router", "attr": "router", "profiles": {"all", "default", "podcast"}}, - {"name": "youtube", "module": "api.youtube.router", "attr": "router", "profiles": {"all", "default"}, "include_kwargs": {"prefix": "/api"}}, - {"name": "research_config", "module": "api.research_config", "attr": "router", "profiles": {"all", "default"}, "include_kwargs": {"prefix": "/api/research", "tags": ["research"]}}, - {"name": "research_engine", "module": "api.research.router", "attr": "router", "profiles": {"all", "default"}, "include_kwargs": {"tags": ["Research Engine"]}}, - {"name": "scheduler_dashboard", "module": "api.scheduler_dashboard", "attr": "router", "profiles": {"all", "default"}}, - {"name": "oauth_token_monitoring", "module": "api.oauth_token_monitoring_routes", "attr": "router", "profiles": {"all", "default"}}, - {"name": "agents", "module": "api.agents_api", "attr": "router", "profiles": {"all", "default"}}, - {"name": "today_workflow", "module": "api.today_workflow", "attr": "router", "profiles": {"all", "default"}}, + {"name": "blog_writer", "module": "api.blog_writer.router", "attr": "router", "features": {"all", "blog-writer"}}, + {"name": "story_writer", "module": "api.story_writer.router", "attr": "router", "features": {"all", "story-writer"}}, + {"name": "wix", "module": "api.wix_routes", "attr": "router", "features": {"all"}}, + {"name": "blog_seo_analysis", "module": "api.blog_writer.seo_analysis", "attr": "router", "features": {"all", "blog-writer"}}, + {"name": "persona", "module": "api.persona_routes", "attr": "router", "features": {"all", "persona"}}, + {"name": "video_studio", "module": "api.video_studio.router", "attr": "router", "features": {"all", "video-studio"}}, + {"name": "stability", "module": "routers.stability", "attr": "router", "features": {"all", "image-studio"}}, + {"name": "stability_advanced", "module": "routers.stability_advanced", "attr": "router", "features": {"all", "image-studio"}}, + {"name": "stability_admin", "module": "routers.stability_admin", "attr": "router", "features": {"all", "image-studio"}}, + {"name": "images", "module": "api.images", "attr": "router", "features": {"all", "image-studio"}}, + {"name": "image_studio", "module": "routers.image_studio", "attr": "router", "features": {"all", "image-studio"}}, + {"name": "product_marketing", "module": "routers.product_marketing", "attr": "router", "features": {"all", "product-marketing"}}, + {"name": "campaign_creator", "module": "routers.campaign_creator", "attr": "router", "features": {"all"}}, + {"name": "content_assets", "module": "api.content_assets.router", "attr": "router", "features": {"all"}}, + {"name": "podcast", "module": "api.podcast.router", "attr": "router", "features": {"all", "podcast"}}, + {"name": "youtube", "module": "api.youtube.router", "attr": "router", "features": {"all", "youtube"}, "include_kwargs": {"prefix": "/api"}}, + {"name": "research_config", "module": "api.research_config", "attr": "router", "features": {"all", "research"}, "include_kwargs": {"prefix": "/api/research", "tags": ["research"]}}, + {"name": "research_engine", "module": "api.research.router", "attr": "router", "features": {"all", "research"}, "include_kwargs": {"tags": ["Research Engine"]}}, + {"name": "scheduler_dashboard", "module": "api.scheduler_dashboard", "attr": "router", "features": {"all", "scheduler"}}, + {"name": "oauth_token_monitoring", "module": "api.oauth_token_monitoring_routes", "attr": "router", "features": {"all", "core"}}, + {"name": "agents", "module": "api.agents_api", "attr": "router", "features": {"all"}}, + {"name": "today_workflow", "module": "api.today_workflow", "attr": "router", "features": {"all"}}, ] OPTIONAL_MODULE_MATRIX = { @@ -81,15 +81,50 @@ class RouterManager: self.failed_routers = [] self.skipped_routers = [] + @staticmethod + def get_enabled_features() -> set: + """Get enabled features from environment variable. + + ALWRITY_ENABLED_FEATURES can be: + - "all" - enable all features (default) + - comma-separated list: "podcast,blog-writer,youtube" + - single feature: "podcast" + """ + env_value = os.getenv( + "ALWRITY_ENABLED_FEATURES", + os.getenv("ALWRITY_FEATURE_PROFILE", os.getenv("ALWRITY_ROUTER_PROFILE", "all")) + ).strip().lower() + + if not env_value or env_value == "all": + return {"all"} + + return {f.strip() for f in env_value.split(",") if f.strip()} + def _is_verbose(self) -> bool: return os.getenv("ALWRITY_VERBOSE", "false").lower() == "true" def _get_profile(self) -> str: - return os.getenv("ALWRITY_FEATURE_PROFILE", os.getenv("ALWRITY_ROUTER_PROFILE", os.getenv("ALWRITY_FEATURE_TO_ENABLE", "all"))).strip().lower() or "all" + """Legacy method - returns primary profile.""" + enabled = self.get_enabled_features() + if "all" in enabled: + return "all" + # Return first feature as profile for backwards compatibility + return list(enabled)[0] if enabled else "all" - def _should_include_router(self, registry_entry: Dict[str, Any], profile: str) -> bool: - profiles = registry_entry.get("profiles", {"all", "default"}) - return profile in profiles or profile in {"all", "default"} + def _should_include_router(self, registry_entry: Dict[str, Any], enabled_features: set) -> bool: + """Check if router should be included based on enabled features.""" + required_features = registry_entry.get("features", set()) + + # If "all" is enabled, include everything + if "all" in enabled_features: + return True + + # If no required features specified, include by default + if not required_features: + return True + + # Check if any required feature is enabled + return bool(required_features & enabled_features) def _load_router_from_registry(self, registry_entry: Dict[str, Any]): module = import_module(registry_entry["module"]) @@ -120,15 +155,15 @@ class RouterManager: def _include_registry_group(self, registry: List[Dict[str, Any]], group_name: str) -> bool: verbose = self._is_verbose() - profile = self._get_profile() + enabled_features = self.get_enabled_features() try: if verbose: - logger.info(f"Including {group_name} routers for profile '{profile}'...") + logger.info(f"Including {group_name} routers with features: {enabled_features}...") for entry in registry: - if not self._should_include_router(entry, profile): - reason = f"profile '{profile}' not in {entry.get('profiles', set())}" + if not self._should_include_router(entry, enabled_features): + reason = f"features {enabled_features} not matching {entry.get('features', set())}" self.skipped_routers.append({"name": entry["name"], "reason": reason}) if verbose: logger.info(f"⏭️ Skipping {entry['name']}: {reason}") @@ -140,7 +175,7 @@ class RouterManager: except Exception as e: logger.warning(f"{entry['name']} router not mounted: {e}") - logger.info(f"✅ {group_name.capitalize()} routers processed for profile '{profile}'") + logger.info(f"✅ {group_name.capitalize()} routers processed for features: {enabled_features}") return True except Exception as e: diff --git a/backend/app.py b/backend/app.py index dbe9c3a3..f863996f 100644 --- a/backend/app.py +++ b/backend/app.py @@ -59,18 +59,47 @@ def _env_flag_enabled(*env_names: str) -> bool: return False -PODCAST_ONLY_DEMO_MODE = _env_flag_enabled( - "ALWRITY_PODCAST_ONLY_DEMO_MODE", - "PODCAST_ONLY_DEMO_MODE", -) +def get_enabled_features() -> set: + """Get enabled features from environment variable. + + ALWRITY_ENABLED_FEATURES can be: + - "all" - enable all features (default) + - comma-separated list: "podcast,core" + - single feature: "podcast" + """ + env_value = os.getenv( + "ALWRITY_ENABLED_FEATURES", + os.getenv("ALWRITY_FEATURE_PROFILE", os.getenv("ALWRITY_ROUTER_PROFILE", "all")) + ).strip().lower() + + if not env_value or env_value == "all": + return {"all"} + + return {f.strip() for f in env_value.split(",") if f.strip()} def is_podcast_only_demo_mode() -> bool: - return PODCAST_ONLY_DEMO_MODE + """Check if podcast-only mode is enabled via new or legacy flags.""" + # First check the new consolidated flag + enabled = get_enabled_features() + if "podcast" in enabled and "all" not in enabled: + return True + + # Fall back to legacy flags for backwards compatibility + return _env_flag_enabled( + "ALWRITY_PODCAST_ONLY_DEMO_MODE", + "PODCAST_ONLY_DEMO_MODE" + ) def should_include_non_podcast_features() -> bool: - return not is_podcast_only_demo_mode() + """Check if non-podcast features should be included.""" + enabled = get_enabled_features() + return "all" in enabled or "core" in enabled or "blog-writer" in enabled + + +# Legacy constant for backwards compatibility - prefer using get_enabled_features() +PODCAST_ONLY_DEMO_MODE = is_podcast_only_demo_mode() # Set up clean logging for end users diff --git a/backend/start_alwrity_backend.py b/backend/start_alwrity_backend.py index d4aaed82..254c4b41 100644 --- a/backend/start_alwrity_backend.py +++ b/backend/start_alwrity_backend.py @@ -26,42 +26,63 @@ class BootstrapResult: LINGUISTIC_REQUIRED_FEATURES = {"content_planning", "strategy_copilot", "facebook", "linkedin", "blog_writer", "persona"} +def get_enabled_features() -> set: + """Get enabled features from environment variable. + + ALWRITY_ENABLED_FEATURES can be: + - "all" - enable all features (default) + - comma-separated list: "podcast,blog-writer,youtube" + - single feature: "podcast" + """ + env_value = os.getenv( + "ALWRITY_ENABLED_FEATURES", + os.getenv("ALWRITY_FEATURE_PROFILE", os.getenv("ALWRITY_ROUTER_PROFILE", "all")) + ).strip().lower() + + if not env_value or env_value == "all": + return {"all"} + + return {f.strip() for f in env_value.split(",") if f.strip()} + + def get_active_profile() -> str: - """Get active profile from environment variables.""" - return os.getenv("ALWRITY_ACTIVE_PROFILE", os.getenv("ALWRITY_PROFILE", os.getenv("ALWRITY_FEATURE_PROFILE", os.getenv("ALWRITY_ROUTER_PROFILE", os.getenv("ALWRITY_FEATURE_TO_ENABLE", "all"))))).strip().lower() or "all" - - -def get_loaded_features() -> set: - """Get loaded features from environment variables.""" - features_str = os.getenv("ALWRITY_LOADED_FEATURES", os.getenv("ALWRITY_ENABLED_FEATURES", os.getenv("ALWRITY_FEATURES", ""))) - if not features_str: - return set() - return {f.strip().lower() for f in features_str.split(",") if f.strip()} + """Legacy function - returns primary profile for backwards compatibility.""" + enabled = get_enabled_features() + if "all" in enabled: + return "all" + return list(enabled)[0] if enabled else "all" def should_bootstrap_linguistic_models() -> bool: - """Decide whether to bootstrap linguistic models based on profile.""" - profile = get_active_profile() + """Decide whether to bootstrap linguistic models based on enabled features.""" + enabled_features = get_enabled_features() verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true" - if profile in {"all", "default"}: + if "all" in enabled_features: return True - loaded_features = get_loaded_features() - if not loaded_features: - return False + # Map old profile names to features for backwards compatibility + feature_mapping = { + "podcast": "podcast", + "youtube": "youtube", + "planning": "content-planning", + "default": "all" + } - return bool(loaded_features & LINGUISTIC_REQUIRED_FEATURES) + # Check if any linguistic-required feature is enabled + linguistic_features = {"content_planning", "facebook", "linkedin", "blog-writer", "persona"} + return bool(enabled_features & linguistic_features) def should_bootstrap_local_llm_models() -> bool: - """Decide whether to bootstrap local LLM models based on profile.""" - profile = get_active_profile() + """Decide whether to bootstrap local LLM models based on enabled features.""" + enabled_features = get_enabled_features() - if profile in {"all", "default"}: + if "all" in enabled_features: return True - return profile not in {"podcast", "youtube", "planning"} + # Skip LLM bootstrap for lean deployments + return "core" in enabled_features or "podcast" in enabled_features def bootstrap_linguistic_models() -> BootstrapResult: diff --git a/frontend/src/utils/demoMode.ts b/frontend/src/utils/demoMode.ts index 3cb6a0c9..730096ac 100644 --- a/frontend/src/utils/demoMode.ts +++ b/frontend/src/utils/demoMode.ts @@ -1,47 +1,79 @@ /** - * Demo mode detection utilities for podcast-only demo mode. + * Consolidated feature mode detection utilities. + * + * Uses ALWRITY_ENABLED_FEATURES (backend) / REACT_APP_ENABLED_FEATURES (frontend) + * Format: "all" or comma-separated features: "podcast,core" */ -const DEMO_MODE_STORAGE_KEYS = [ +const ENABLED_FEATURES_STORAGE_KEYS = [ 'app_mode', + 'enabled_features', 'demo_mode', 'podcast_only_demo_mode', ]; -const DEMO_MODE_ENV_KEYS = [ +const ENABLED_FEATURES_ENV_KEYS = [ + 'REACT_APP_ENABLED_FEATURES', 'REACT_APP_APP_MODE', 'REACT_APP_DEMO_MODE', 'REACT_APP_PODCAST_ONLY_DEMO_MODE', ]; /** - * Check if podcast-only demo mode is enabled. - * Checks localStorage first, then environment variables. + * Get enabled features from localStorage or environment. + * Returns a set of enabled feature names. */ -export function isPodcastOnlyDemoMode(): boolean { - // Check localStorage - for (const key of DEMO_MODE_STORAGE_KEYS) { - const value = (localStorage.getItem(key) || '').toLowerCase(); - if (value === 'true' || value === 'podcast-only' || value === 'podcast_only') { - return true; +export function getEnabledFeatures(): Set { + // Check localStorage first + for (const key of ENABLED_FEATURES_STORAGE_KEYS) { + const value = localStorage.getItem(key); + if (value) { + const features = value.toLowerCase().split(',').map(f => f.trim()); + if (features.includes('all')) { + return new Set(['all']); + } + return new Set(features.filter(f => f)); } } // Check environment variables - for (const key of DEMO_MODE_ENV_KEYS) { - const value = (process.env[key] || '').toLowerCase(); - if (value === 'true' || value === 'podcast-only' || value === 'podcast_only') { - return true; + for (const key of ENABLED_FEATURES_ENV_KEYS) { + const value = process.env[key]; + if (value) { + const features = value.toLowerCase().split(',').map(f => f.trim()); + if (features.includes('all')) { + return new Set(['all']); + } + return new Set(features.filter(f => f)); } } - return false; + // Default: all features enabled + return new Set(['all']); +} + +/** + * Check if a specific feature is enabled. + */ +export function isFeatureEnabled(feature: string): boolean { + const enabled = getEnabledFeatures(); + return enabled.has('all') || enabled.has(feature); +} + +/** + * Check if podcast-only mode is enabled. + * Returns true when podcast is enabled but not "all". + */ +export function isPodcastOnlyDemoMode(): boolean { + const enabled = getEnabledFeatures(); + return enabled.has('podcast') && !enabled.has('all'); } /** * Check if the app should skip onboarding entirely. - * Returns true in podcast-only demo mode. + * Returns true in podcast-only demo mode or when not using all features. */ export function shouldSkipOnboarding(): boolean { - return isPodcastOnlyDemoMode(); + const enabled = getEnabledFeatures(); + return enabled.has('podcast') || !enabled.has('all'); }