1 line
148 KiB
JSON
1 line
148 KiB
JSON
{"repo": "anioko/archie-ea", "files": {"config.py": {"3571dfac9ad1eea74a1dbda82193021ee41f7390": {"t": 1785745455.5445857, "n": 21096, "h": "11e3d14837367662", "c": "import os\nfrom datetime import timedelta\n\n# Optional raygun import\ntry:\n from raygun4py.middleware import flask as flask_raygun\n\n HAS_RAYGUN = True\nexcept ImportError:\n HAS_RAYGUN = False\n flask_raygun = None\n\n# Python 3 only (Python 2 is no longer supported)\nimport urllib.parse\n\nbasedir = os.path.abspath(os.path.dirname(__file__))\n\n# Canonical module configuration. The v2 (\"guardrail\") modules are the active code\n# path (see CLAUDE.md), and the navigation/templates reference their endpoints, so\n# they MUST be registered by default. Without these flags a fresh install registers\n# only legacy/partial blueprints and every authenticated page 500s on a sidebar\n# url_for to an unregistered endpoint (e.g. solution_design.list_solutions). Using\n# setdefault means an explicit env value (true/false) is still respected.\nfor _canonical_flag in (\n \"USE_SOLUTIONS_STRATEGIC_GUARDRAILS\",\n \"USE_NEW_SOLUTIONS_STRATEGIC\",\n \"USE_CAPABILITIES_GUARDRAILS\",\n \"USE_NEW_CAPABILITIES\",\n \"USE_DASHBOARD_GUARDRAILS\",\n \"USE_NEW_DASHBOARD\",\n \"USE_APPLICATIONS_GUARDRAILS\",\n \"USE_NEW_APPLICATIONS\",\n \"USE_ARCHITECTURE_GUARDRAILS\",\n \"USE_NEW_ARCHITECTURE\",\n \"USE_AI_CHAT_GUARDRAILS\",\n \"USE_NEW_AI_CHAT\",\n # NOTE: vendors are intentionally NOT defaulted to the v2/guardrail path.\n # The always-on unified vendor blueprints serve the vendor pages AND the\n # /api/vendors/* JSON the vendor dashboard widgets call (list, ranking).\n # Forcing USE_VENDORS_GUARDRAILS switches to v2-only and drops the unified\n # API, 404-ing those widgets (\"not found\" in the UI).\n):\n os.environ.setdefault(_canonical_flag, \"true\")\n\n\ndef _env_bool(name: str, default: bool) -> bool:\n value = os.environ.get(name)\n if value is None:\n return default\n return value.strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\n\n# Load environment variables from .env file\nfrom dotenv import load_dotenv\n\n# Load from .env file using explicit path (ensures it works regardless of working directory)\nenv_path = os.path.join(basedir, \".env\")\nif os.path.exists(env_path):\n load_dotenv(env_path)\n print(f\"Loaded environment from: {env_path}\")\nelse:\n # Fallback to default behavior\n load_dotenv()\n print(\"Loaded environment from default location\")\n\n# Verify critical API keys are loaded\n_api_keys_loaded = []\nfor key in [\n \"OPENAI_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"GEMINI_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"HUGGINGFACE_API_KEY\",\n]:\n if os.getenv(key):\n _api_keys_loaded.append(key)\nif _api_keys_loaded:\n print(f\"API keys loaded: {', '.join(_api_keys_loaded)}\")\nelse:\n print(\"WARNING: No LLM API keys found in environment!\")\n\n\nclass Config:\n APP_NAME = os.environ.get(\"APP_NAME\", \"A.R.C.H.I.E.\")\n SECRET_KEY = os.environ.get(\"SECRET_KEY\")\n if not SECRET_KEY:\n import secrets\n\n SECRET_KEY = secrets.token_hex(32)\n print(\n \"WARNING: SECRET_KEY env var not set. A random key has been generated \"\n \"for this session. Sessions will NOT persist across restarts. \"\n \"Set SECRET_KEY in your .env file for production use.\"\n )\n SQLALCHEMY_COMMIT_ON_TEARDOWN = True\n\n # Session security — 8-hour session lifetime, 30-day remember-me cookie\n PERMANENT_SESSION_LIFETIME = timedelta(hours=8)\n REMEMBER_COOKIE_DURATION = timedelta(days=30)\n SESSION_REFRESH_EACH_REQUEST = True # FAR-012: Ensure session is refreshed on every request\n REMEMBER_COOKIE_REFRESH_EACH_REQUEST = True # FAR-012: Refresh remember-me cookie on activity\n\n # JWT Configuration\n JWT_TOKEN_LOCATION = [\"headers\"]\n JWT_SECRET_KEY = os.environ.get(\"JWT_SECRET_KEY\") or SECRET_KEY\n JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1)\n\n # Email\n MAIL_SERVER = os.environ.get(\"MAIL_SERVER\", \"smtp.sendgrid.net\")\n MAIL_PORT = os.environ.get(\"MAIL_PORT\", 587)\n MAIL_USE_TLS = os.environ.get(\"MAIL_USE_TLS\", True)\n MAIL_USE_SSL = os.environ.get(\"MAIL_USE_SSL\", False)\n MAIL_USERNAME = os.environ.get(\"MAIL_USERNAME\")\n MAIL_PASSWORD = os.environ.get(\"MAIL_PASSWORD\")\n MAIL_DEFAULT_SENDER = os.environ.get(\"MAIL_DEFAULT_SENDER\")\n\n # Analytics\n GOOGLE_ANALYTICS_ID = os.environ.get(\"GOOGLE_ANALYTICS_ID\", \"\")\n SEGMENT_API_KEY = os.environ.get(\"SEGMENT_API_KEY\", \"\")\n\n # Admin account\n ADMIN_PASSWORD = os.environ.get(\"ADMIN_PASSWORD\")\n if not ADMIN_PASSWORD:\n import secrets\n\n ADMIN_PASSWORD = secrets.token_urlsafe(16)\n print(\n \"WARNING: ADMIN_PASSWORD env var not set. Generated random password. \"\n \"Set ADMIN_PASSWORD in your .env file for production use.\"\n )\n ADMIN_EMAIL = os.environ.get(\"ADMIN_EMAIL\", \"flask-base-admin@example.com\")\n EMAIL_SUBJECT_PREFIX = \"[{}]\".format(APP_NAME)\n EMAIL_SENDER = \"{app_name} Admin <{email}>\".format(\n app_name=APP_NAME, email=MAIL_USERNAME\n )\n\n # ARCHIE Deploy: Credential encryption + Coolify PaaS + n8n connector sync\n CREDENTIAL_ENCRYPTION_KEY = os.environ.get(\"CREDENTIAL_ENCRYPTION_KEY\", \"\")\n COOLIFY_API_URL = os.environ.get(\"COOLIFY_API_URL\", \"http://localhost:8000\")\n COOLIFY_API_TOKEN = os.environ.get(\"COOLIFY_API_TOKEN\", \"\")\n COOLIFY_DOMAIN_SUFFIX = os.environ.get(\"COOLIFY_DOMAIN_SUFFIX\", \"archie.example.com\")\n N8N_API_URL = os.environ.get(\"N8N_API_URL\", \"http://localhost:5678\")\n N8N_API_TOKEN = os.environ.get(\"N8N_API_TOKEN\", \"\")\n\n REDIS_URL = os.getenv(\"REDIS_URL\") or os.getenv(\n \"REDISTOGO_URL\", \"redis://localhost:6379/0\"\n )\n\n RAYGUN_APIKEY = os.environ.get(\"RAYGUN_APIKEY\")\n\n # Stripe Billing (COM-001) — platform works without these; set in production .env\n STRIPE_SECRET_KEY = os.environ.get(\"STRIPE_SECRET_KEY\", \"\")\n STRIPE_WEBHOOK_SECRET = os.environ.get(\"STRIPE_WEBHOOK_SECRET\", \"\")\n STRIPE_PRICE_PRO = os.environ.get(\"STRIPE_PRICE_PRO\", \"\")\n STRIPE_PRICE_ENTERPRISE = os.environ.get(\"STRIPE_PRICE_ENTERPRISE\", \"\")\n\n # Parse the REDIS_URL to set RQ config variables\n urllib.parse.uses_netloc.append(\"redis\")\n url = urllib.parse.urlparse(REDIS_URL)\n\n RQ_DEFAULT_HOST = url.hostname\n RQ_DEFAULT_PORT = url.port\n RQ_DEFAULT_PASSWORD = url.password\n RQ_DEFAULT_DB = 0\n\n # Celery configuration (async task queue)\n # Feature-flagged: set CELERY_ENABLED=true to enable async batch import\n CELERY_ENABLED = os.environ.get(\"CELERY_ENABLED\", \"false\").lower() == \"true\"\n CELERY_BROKER_URL = os.environ.get(\"CELERY_BROKER_URL\", REDIS_URL)\n CELERY_RESULT_BACKEND = os.environ.get(\"CELERY_RESULT_BACKEND\", REDIS_URL)\n CELERY_TASK_SERIALIZER = \"json\"\n CELERY_ACCEPT_CONTENT = [\"json\"]\n CELERY_RESULT_SERIALIZER = \"json\"\n CELERY_TIMEZONE = \"UTC\"\n CELERY_ENABLE_UTC = True\n\n # SSO / Enterprise Identity (S0-01)\n # Supports Azure AD, Okta (OIDC) and any SAML 2.0 IdP (PLT-030).\n # Feature-flagged: SSO routes only active when FeatureFlag(key='sso_authentication') is enabled.\n SSO_PROVIDERS = {\n \"azure\": {\n \"client_id\": os.environ.get(\"AZURE_AD_CLIENT_ID\", \"\"),\n \"client_secret\": os.environ.get(\"AZURE_AD_CLIENT_SECRET\", \"\"),\n \"server_metadata_url\": os.environ.get(\n \"AZURE_AD_METADATA_URL\",\n \"https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration\".format(\n tenant=os.environ.get(\"AZURE_AD_TENANT_ID\", \"common\")\n ),\n ),\n \"client_kwargs\": {\"scope\": \"openid email profile\"},\n },\n \"okta\": {\n \"client_id\": os.environ.get(\"OKTA_CLIENT_ID\", \"\"),\n \"client_secret\": os.environ.get(\"OKTA_CLIENT_SECRET\", \"\"),\n \"server_metadata_url\": os.environ.get(\"OKTA_METADATA_URL\", \"\"),\n \"client_kwargs\": {\"scope\": \"openid email profile\"},\n },\n }\n\n # SAML 2.0 IdP configuration (PLT-030)\n # Set SAML_IDP_SSO_URL + SAML_SP_ENTITY_ID to enable SAML alongside OIDC.\n # Supported IdPs: ADFS, PingFederate, Shibboleth, Okta (SAML), Azure AD (SAML).\n # Routes registered at /account/saml/login, /account/saml/acs, /account/saml/metadata.\n SAML_IDP_SSO_URL = os.environ.get(\"SAML_IDP_SSO_URL\", \"\")\n # IdP Entity ID / Issuer URI (validated against Assertion Issuer element)\n SAML_IDP_ENTITY_ID = os.environ.get(\"SAML_IDP_ENTITY_ID\", \"\")\n # IdP X.509 signing certificate — PEM body without -----BEGIN/END----- headers.\n # Required for production signature validation (via xmlsec / python3-saml).\n SAML_IDP_CERT = os.environ.get(\"SAML_IDP_CERT\", \"\")\n # SP Entity ID — typically the platform's base URL\n SAML_SP_ENTITY_ID = os.environ.get(\"SAML_SP_ENTITY_ID\", \"\")\n # SP ACS URL — leave empty to auto-derive from url_for('account.saml_acs')\n SAML_SP_ACS_URL = os.environ.get(\"SAML_SP_ACS_URL\", \"\")\n\n # Internationalization (S2-01) — date/number/currency formatting\n # Full string translation (gettext) is Phase 2.\n BABEL_DEFAULT_LOCALE = os.environ.get(\"BABEL_DEFAULT_LOCALE\", \"en\")\n BABEL_DEFAULT_TIMEZONE = os.environ.get(\"BABEL_DEFAULT_TIMEZONE\", \"UTC\")\n\n # Read Replica Configuration (ENH-012)\n # Set DATABASE_READ_REPLICA_URL to a replica DB connection string to offload\n # dashboard and portfolio GET queries from the primary. When not configured,\n # all queries use the default SQLALCHEMY_DATABASE_URI (no-op).\n DATABASE_READ_REPLICA_URL = os.environ.get(\"DATABASE_READ_REPLICA_URL\", \"\")\n USE_READ_REPLICA_FOR_DASHBOARD = (\n os.environ.get(\"USE_READ_REPLICA_FOR_DASHBOARD\", \"false\").lower() == \"true\"\n )\n\n # AI-originated CRUD approval gate (A95-008)\n # When true, AI CRUD endpoints return 202 pending_approval instead of writing directly.\n REQUIRE_AI_APPROVAL = os.environ.get(\"REQUIRE_AI_APPROVAL\", \"false\").lower() == \"true\"\n\n # North Star Navigation (NORTH-STAR-001)\n # Enterprise-grade navigation for Fortune 500 TOGAF/ArchiMate practitioners.\n # When enabled: renamed navigation items, persona-based filtering, hidden admin items.\n # Rollback: set to false and restart service (zero risk, instant revert).\n # ENABLED 2026-04-02: Quick wins from NORTH_STAR_ASSESSMENT.md Part 10\n ENABLE_NORTH_STAR_NAV = _env_bool(\"ENABLE_NORTH_STAR_NAV\", True)\n\n # North Star Navigation Phase 2 (NORTH-STAR-002)\n # Complete ArchiMate 3.2 layer-based navigation with all 55 element types.\n # When enabled: 6-section IA, layer-first architecture, Portfolio/Architecture separation.\n # Depends on: ENABLE_NORTH_STAR_NAV=True (Phase 1 must be active)\n # Rollback: set to false and restart service.\n ENABLE_NORTH_STAR_PHASE2 = _env_bool(\"ENABLE_NORTH_STAR_PHASE2\", False)\n\n # Page-aware AI guide is fail-closed by default and only activates when\n # this flag is explicitly enabled and an LLM provider is configured.\n AI_PAGE_GUIDE_ENABLED = _env_bool(\"AI_PAGE_GUIDE_ENABLED\", False)\n\n # File Upload Settings\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max file size\n ALLOWED_EXTENSIONS = {\n \"pdf\",\n \"docx\",\n \"doc\",\n \"xlsx\",\n \"xls\",\n \"png\",\n \"jpg\",\n \"jpeg\",\n \"vsdx\",\n \"txt\",\n \"csv\",\n \"md\",\n }\n UPLOAD_FOLDER = os.path.join(basedir, \"uploads\", \"documents\")\n\n @staticmethod\n def init_app(app):\n \"\"\"Base class hook — subclasses override to customize app initialization.\"\"\"\n # Behind a TLS-terminating reverse proxy (Caddy/nginx/Traefik), trust the\n # X-Forwarded-* headers so request.scheme/host reflect the real https URL.\n # Without this the app builds http:// URLs behind https, causing\n # mixed-content blocking. Opt-in via TRUST_PROXY so forged headers are not\n # trusted when the app is exposed directly.\n if os.environ.get(\"TRUST_PROXY\", \"false\").lower() in (\"1\", \"true\", \"yes\") \\\n and not getattr(app, \"_proxyfix_applied\", False):\n from werkzeug.middleware.proxy_fix import ProxyFix\n app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)\n app._proxyfix_applied = True\n return\n\n\nclass DevelopmentConfig(Config):\n DEBUG = True\n ASSETS_DEBUG = True\n\n # N+1 query detection — set SQLALCHEMY_ECHO=true in .env to enable query logging\n SQLALCHEMY_ECHO = os.environ.get(\"SQLALCHEMY_ECHO\", \"false\").lower() == \"true\"\n JIRA_AUTO_PUSH = os.environ.get(\"JIRA_AUTO_PUSH\", \"false\").lower() == \"true\"\n\n # PostgreSQL configuration (REQUIRED - validated at runtime)\n # Default to a local Postgres instance for development when DATABASE_URL is not set.\n # Standard port 5432; set DATABASE_URL in .env to override (e.g. Windows installs\n # that run Postgres on 5439).\n SQLALCHEMY_DATABASE_URI = os.environ.get(\n \"DATABASE_URL\",\n \"postgresql://postgres:postgres@127.0.0.1:5432/archie\", # secrets-safety-ok\n )\n\n # SERVER_NAME must NOT be hardcoded — Flask-WTF CSRF validates the Referer\n # header against SERVER_NAME, so a hardcoded value causes \"CSRF token is missing\"\n # when the browser omits the Referer header (privacy settings, extensions, etc.).\n # Only set when explicitly provided via environment variable.\n SERVER_NAME = os.environ.get(\"SERVER_NAME\") or None\n PREFERRED_URL_SCHEME = os.environ.get(\"PREFERRED_URL_SCHEME\", \"http\")\n APPLICATION_ROOT = os.environ.get(\"APPLICATION_ROOT\", \"/\")\n\n # PostgreSQL connection options (optimized for local development)\n SQLALCHEMY_ENGINE_OPTIONS = {\n \"pool_pre_ping\": True,\n \"pool_recycle\": 300,\n \"pool_size\": 5,\n \"max_overflow\": 10,\n \"pool_timeout\": 30,\n \"connect_args\": {\n \"client_encoding\": \"utf8\",\n \"options\": \"-c client_encoding=utf8\",\n },\n \"pool_reset_on_return\": \"commit\",\n }\n\n @classmethod\n def init_app(cls, app):\n # Ensure a sensible default exists for development (Postgres is the standard)\n if not app.config.get(\"SQLALCHEMY_DATABASE_URI\"):\n app.logger.warning(\n \"No DATABASE_URL provided; falling back to local Postgres default.\"\n )\n\n\nclass TestingConfig(Config):\n TESTING = True\n WTF_CSRF_ENABLED = False\n\n # PostgreSQL REQUIRED for tests (matches production behavior)\n # SQLite is NOT supported - tests must use PostgreSQL for consistency\n SQLALCHEMY_DATABASE_URI = os.environ.get(\n \"TEST_DATABASE_URL\",\n \"postgresql://postgres:postgres@127.0.0.1:5432/archie_test\", # secrets-safety-ok\n )\n\n # PostgreSQL connection options for testing\n SQLALCHEMY_ENGINE_OPTIONS = {\n \"pool_pre_ping\": True,\n \"pool_size\": 5,\n \"max_overflow\": 10,\n }\n\n @classmethod\n def init_app(cls, app):\n # Validate PostgreSQL is being used (not SQLite)\n db_uri = app.config.get(\"SQLALCHEMY_DATABASE_URI\", \"\")\n if \"sqlite\" in db_uri.lower():\n raise ValueError(\n \"TEST_DATABASE_URL must be PostgreSQL, not SQLite. \"\n \"Set TEST_DATABASE_URL to a PostgreSQL connection string. \"\n \"Example: postgresql://postgres:postgres@localhost:5432/flask_test\" # secrets-safety-ok\n )\n\n if not db_uri:\n app.logger.warning(\n \"No TEST_DATABASE_URL provided; using default local PostgreSQL test database.\"\n )\n\n print(\"THIS APP IS IN TESTING MODE. YOU SHOULD NOT SEE THIS IN PRODUCTION.\")\n\n\nclass ProductionConfig(Config):\n DEBUG = False\n USE_RELOADER = False\n\n # Session cookie security\n _cookie_secure_default = os.environ.get(\"PREFERRED_URL_SCHEME\", \"http\").lower() == \"https\"\n SESSION_COOKIE_SECURE = _env_bool(\"SESSION_COOKIE_SECURE\", _cookie_secure_default)\n SESSION_COOKIE_HTTPONLY = True\n SESSION_COOKIE_SAMESITE = \"Lax\"\n REMEMBER_COOKIE_SECURE = _env_bool(\"REMEMBER_COOKIE_SECURE\", _cookie_secure_default)\n REMEMBER_COOKIE_HTTPONLY = True\n\n # PostgreSQL for production (REQUIRED - validated at runtime)\n SQLALCHEMY_DATABASE_URI = os.environ.get(\"DATABASE_URL\")\n SSL_DISABLE = os.environ.get(\"SSL_DISABLE\", \"False\") == \"True\"\n JIRA_AUTO_PUSH = os.environ.get(\"JIRA_AUTO_PUSH\", \"false\").lower() == \"true\"\n\n # PostgreSQL connection options (optimized for production)\n SQLALCHEMY_ENGINE_OPTIONS = {\n \"pool_pre_ping\": True,\n \"pool_recycle\": 300,\n \"pool_size\": 20,\n \"max_overflow\": 30,\n \"pool_timeout\": 30,\n \"connect_args\": {\n \"client_encoding\": \"utf8\",\n \"options\": \"-c client_encoding=utf8\",\n },\n \"pool_reset_on_return\": \"commit\",\n }\n\n @classmethod\n def init_app(cls, app):\n # Validate DATABASE_URL is set\n if not app.config.get(\"SQLALCHEMY_DATABASE_URI\"):\n raise ValueError(\n \"DATABASE_URL environment variable is required. PostgreSQL must be configured for production.\"\n )\n\n Config.init_app(app)\n assert os.environ.get(\"SECRET_KEY\"), \"SECRET_KEY IS NOT SET!\"\n\n # Trust the nginx reverse proxy's forwarded headers (X-Forwarded-Proto /\n # -For) so request.scheme/host reflect the real client-facing protocol.\n # Without this, behind TLS the app still builds http:// external URLs\n # (breaking OAuth redirect URIs, email links) and treats requests as\n # insecure. nginx is the single trusted hop. Idempotent so a subclass\n # (Heroku/Unix) re-running init_app cannot double-wrap.\n if not getattr(app, \"_proxyfix_applied\", False):\n from werkzeug.middleware.proxy_fix import ProxyFix\n\n app.wsgi_app = ProxyFix(\n app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1\n )\n app._proxyfix_applied = True\n\n if HAS_RAYGUN and flask_raygun and app.config.get(\"RAYGUN_APIKEY\"):\n flask_raygun.Provider(app, app.config[\"RAYGUN_APIKEY\"]).attach()\n\n\nclass HerokuConfig(ProductionConfig):\n @classmethod\n def init_app(cls, app):\n # ProductionConfig.init_app applies ProxyFix (idempotently); no need to\n # re-wrap here.\n ProductionConfig.init_app(app)\n\n\nclass UnixConfig(ProductionConfig):\n @classmethod\n def init_app(cls, app):\n ProductionConfig.init_app(app)\n\n # Log to syslog\n import logging\n from logging.handlers import SysLogHandler\n\n syslog_handler = SysLogHandler()\n syslog_handler.setLevel(logging.WARNING)\n app.logger.addHandler(syslog_handler)\n\n\n# Currency Configuration System\nclass CurrencyConfig:\n \"\"\"Centralized currency configuration for the application\"\"\"\n\n DEFAULT_CURRENCY = os.environ.get(\"DEFAULT_CURRENCY\", \"GBP\")\n\n SUPPORTED_CURRENCIES = {\n \"GBP\": {\n \"symbol\": \"£\",\n \"code\": \"GBP\",\n \"position\": \"prefix\", # prefix or suffix\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"British Pound Sterling\",\n },\n \"USD\": {\n \"symbol\": \"$\",\n \"code\": \"USD\",\n \"position\": \"prefix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"United States Dollar\",\n },\n \"EUR\": {\n \"symbol\": \"€\",\n \"code\": \"EUR\",\n \"position\": \"suffix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \".\",\n \"name\": \"Euro\",\n },\n \"JPY\": {\n \"symbol\": \"¥\",\n \"code\": \"JPY\",\n \"position\": \"prefix\",\n \"decimal_places\": 0,\n \"thousands_separator\": \",\",\n \"name\": \"Japanese Yen\",\n },\n \"CAD\": {\n \"symbol\": \"C$\",\n \"code\": \"CAD\",\n \"position\": \"prefix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"Canadian Dollar\",\n },\n \"AUD\": {\n \"symbol\": \"A$\",\n \"code\": \"AUD\",\n \"position\": \"prefix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"Australian Dollar\",\n },\n }\n\n @classmethod\n def get_currency_config(cls, currency_code=None):\n \"\"\"Get currency configuration for specified currency code\"\"\"\n if not currency_code:\n currency_code = cls.DEFAULT_CURRENCY\n\n config = cls.SUPPORTED_CURRENCIES.get(currency_code)\n if not config:\n # Fallback to default currency instead of crashing\n config = cls.SUPPORTED_CURRENCIES.get(cls.DEFAULT_CURRENCY)\n\n return config\n\n @classmethod\n def is_supported(cls, currency_code):\n \"\"\"Check if currency is supported\"\"\"\n return currency_code in cls.SUPPORTED_CURRENCIES\n\n @classmethod\n def get_all_supported_codes(cls):\n \"\"\"Get list of all supported currency codes\"\"\"\n return list(cls.SUPPORTED_CURRENCIES.keys())\n\n\nconfig = {\n \"development\": DevelopmentConfig,\n \"testing\": TestingConfig,\n \"production\": ProductionConfig,\n \"default\": DevelopmentConfig,\n \"heroku\": HerokuConfig,\n \"unix\": UnixConfig,\n}\n"}, "3289a9251ce21ff4373486164e966b4bd0c6f400": {"t": 1785753429.935331, "n": 21566, "h": "d9fd972e9853166c", "c": "import os\nfrom datetime import timedelta\n\n# Optional raygun import\ntry:\n from raygun4py.middleware import flask as flask_raygun\n\n HAS_RAYGUN = True\nexcept ImportError:\n HAS_RAYGUN = False\n flask_raygun = None\n\n# Python 3 only (Python 2 is no longer supported)\nimport urllib.parse\n\nbasedir = os.path.abspath(os.path.dirname(__file__))\n\n# Canonical module configuration. The v2 (\"guardrail\") modules are the active code\n# path (see CLAUDE.md), and the navigation/templates reference their endpoints, so\n# they MUST be registered by default. Without these flags a fresh install registers\n# only legacy/partial blueprints and every authenticated page 500s on a sidebar\n# url_for to an unregistered endpoint (e.g. solution_design.list_solutions). Using\n# setdefault means an explicit env value (true/false) is still respected.\nfor _canonical_flag in (\n \"USE_SOLUTIONS_STRATEGIC_GUARDRAILS\",\n \"USE_NEW_SOLUTIONS_STRATEGIC\",\n \"USE_CAPABILITIES_GUARDRAILS\",\n \"USE_NEW_CAPABILITIES\",\n \"USE_DASHBOARD_GUARDRAILS\",\n \"USE_NEW_DASHBOARD\",\n \"USE_APPLICATIONS_GUARDRAILS\",\n \"USE_NEW_APPLICATIONS\",\n \"USE_ARCHITECTURE_GUARDRAILS\",\n \"USE_NEW_ARCHITECTURE\",\n \"USE_AI_CHAT_GUARDRAILS\",\n \"USE_NEW_AI_CHAT\",\n # NOTE: vendors are intentionally NOT defaulted to the v2/guardrail path.\n # The always-on unified vendor blueprints serve the vendor pages AND the\n # /api/vendors/* JSON the vendor dashboard widgets call (list, ranking).\n # Forcing USE_VENDORS_GUARDRAILS switches to v2-only and drops the unified\n # API, 404-ing those widgets (\"not found\" in the UI).\n):\n os.environ.setdefault(_canonical_flag, \"true\")\n\n\ndef _env_bool(name: str, default: bool) -> bool:\n value = os.environ.get(name)\n if value is None:\n return default\n return value.strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\n\n# Load environment variables from .env file\nfrom dotenv import load_dotenv\n\n# Load from .env file using explicit path (ensures it works regardless of working directory)\nenv_path = os.path.join(basedir, \".env\")\nif os.path.exists(env_path):\n load_dotenv(env_path)\n print(f\"Loaded environment from: {env_path}\")\nelse:\n # Fallback to default behavior\n load_dotenv()\n print(\"Loaded environment from default location\")\n\n# Verify critical API keys are loaded\n_api_keys_loaded = []\nfor key in [\n \"OPENAI_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"GEMINI_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"HUGGINGFACE_API_KEY\",\n]:\n if os.getenv(key):\n _api_keys_loaded.append(key)\nif _api_keys_loaded:\n print(f\"API keys loaded: {', '.join(_api_keys_loaded)}\")\nelse:\n print(\"WARNING: No LLM API keys found in environment!\")\n\n\nclass Config:\n APP_NAME = os.environ.get(\"APP_NAME\", \"A.R.C.H.I.E.\")\n SECRET_KEY = os.environ.get(\"SECRET_KEY\")\n if not SECRET_KEY:\n import secrets\n\n SECRET_KEY = secrets.token_hex(32)\n print(\n \"WARNING: SECRET_KEY env var not set. A random key has been generated \"\n \"for this session. Sessions will NOT persist across restarts. \"\n \"Set SECRET_KEY in your .env file for production use.\"\n )\n SQLALCHEMY_COMMIT_ON_TEARDOWN = True\n\n # Session security — 8-hour session lifetime, 30-day remember-me cookie\n PERMANENT_SESSION_LIFETIME = timedelta(hours=8)\n REMEMBER_COOKIE_DURATION = timedelta(days=30)\n SESSION_REFRESH_EACH_REQUEST = True # FAR-012: Ensure session is refreshed on every request\n REMEMBER_COOKIE_REFRESH_EACH_REQUEST = True # FAR-012: Refresh remember-me cookie on activity\n\n # JWT Configuration\n JWT_TOKEN_LOCATION = [\"headers\"]\n JWT_SECRET_KEY = os.environ.get(\"JWT_SECRET_KEY\") or SECRET_KEY\n JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1)\n\n # Email\n MAIL_SERVER = os.environ.get(\"MAIL_SERVER\", \"smtp.sendgrid.net\")\n MAIL_PORT = os.environ.get(\"MAIL_PORT\", 587)\n MAIL_USE_TLS = os.environ.get(\"MAIL_USE_TLS\", True)\n MAIL_USE_SSL = os.environ.get(\"MAIL_USE_SSL\", False)\n MAIL_USERNAME = os.environ.get(\"MAIL_USERNAME\")\n MAIL_PASSWORD = os.environ.get(\"MAIL_PASSWORD\")\n MAIL_DEFAULT_SENDER = os.environ.get(\"MAIL_DEFAULT_SENDER\")\n\n # Analytics\n GOOGLE_ANALYTICS_ID = os.environ.get(\"GOOGLE_ANALYTICS_ID\", \"\")\n SEGMENT_API_KEY = os.environ.get(\"SEGMENT_API_KEY\", \"\")\n\n # Admin account\n ADMIN_PASSWORD = os.environ.get(\"ADMIN_PASSWORD\")\n if not ADMIN_PASSWORD:\n import secrets\n\n ADMIN_PASSWORD = secrets.token_urlsafe(16)\n print(\n \"WARNING: ADMIN_PASSWORD env var not set. Generated random password. \"\n \"Set ADMIN_PASSWORD in your .env file for production use.\"\n )\n ADMIN_EMAIL = os.environ.get(\"ADMIN_EMAIL\", \"flask-base-admin@example.com\")\n EMAIL_SUBJECT_PREFIX = \"[{}]\".format(APP_NAME)\n EMAIL_SENDER = \"{app_name} Admin <{email}>\".format(\n app_name=APP_NAME, email=MAIL_USERNAME\n )\n\n # ARCHIE Deploy: Credential encryption + Coolify PaaS + n8n connector sync\n CREDENTIAL_ENCRYPTION_KEY = os.environ.get(\"CREDENTIAL_ENCRYPTION_KEY\", \"\")\n COOLIFY_API_URL = os.environ.get(\"COOLIFY_API_URL\", \"http://localhost:8000\")\n COOLIFY_API_TOKEN = os.environ.get(\"COOLIFY_API_TOKEN\", \"\")\n COOLIFY_DOMAIN_SUFFIX = os.environ.get(\"COOLIFY_DOMAIN_SUFFIX\", \"archie.example.com\")\n N8N_API_URL = os.environ.get(\"N8N_API_URL\", \"http://localhost:5678\")\n N8N_API_TOKEN = os.environ.get(\"N8N_API_TOKEN\", \"\")\n\n REDIS_URL = os.getenv(\"REDIS_URL\") or os.getenv(\n \"REDISTOGO_URL\", \"redis://localhost:6379/0\"\n )\n\n RAYGUN_APIKEY = os.environ.get(\"RAYGUN_APIKEY\")\n\n # Stripe Billing (COM-001) — platform works without these; set in production .env\n STRIPE_SECRET_KEY = os.environ.get(\"STRIPE_SECRET_KEY\", \"\")\n STRIPE_WEBHOOK_SECRET = os.environ.get(\"STRIPE_WEBHOOK_SECRET\", \"\")\n STRIPE_PRICE_PRO = os.environ.get(\"STRIPE_PRICE_PRO\", \"\")\n STRIPE_PRICE_ENTERPRISE = os.environ.get(\"STRIPE_PRICE_ENTERPRISE\", \"\")\n\n # Parse the REDIS_URL to set RQ config variables\n urllib.parse.uses_netloc.append(\"redis\")\n url = urllib.parse.urlparse(REDIS_URL)\n\n RQ_DEFAULT_HOST = url.hostname\n RQ_DEFAULT_PORT = url.port\n RQ_DEFAULT_PASSWORD = url.password\n RQ_DEFAULT_DB = 0\n\n # Celery configuration (async task queue)\n # Feature-flagged: set CELERY_ENABLED=true to enable async batch import\n CELERY_ENABLED = os.environ.get(\"CELERY_ENABLED\", \"false\").lower() == \"true\"\n CELERY_BROKER_URL = os.environ.get(\"CELERY_BROKER_URL\", REDIS_URL)\n CELERY_RESULT_BACKEND = os.environ.get(\"CELERY_RESULT_BACKEND\", REDIS_URL)\n CELERY_TASK_SERIALIZER = \"json\"\n CELERY_ACCEPT_CONTENT = [\"json\"]\n CELERY_RESULT_SERIALIZER = \"json\"\n CELERY_TIMEZONE = \"UTC\"\n CELERY_ENABLE_UTC = True\n\n # SSO / Enterprise Identity (S0-01)\n # Supports Azure AD, Okta (OIDC) and any SAML 2.0 IdP (PLT-030).\n # Feature-flagged: SSO routes only active when FeatureFlag(key='sso_authentication') is enabled.\n SSO_PROVIDERS = {\n \"azure\": {\n \"client_id\": os.environ.get(\"AZURE_AD_CLIENT_ID\", \"\"),\n \"client_secret\": os.environ.get(\"AZURE_AD_CLIENT_SECRET\", \"\"),\n \"server_metadata_url\": os.environ.get(\n \"AZURE_AD_METADATA_URL\",\n \"https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration\".format(\n tenant=os.environ.get(\"AZURE_AD_TENANT_ID\", \"common\")\n ),\n ),\n \"client_kwargs\": {\"scope\": \"openid email profile\"},\n },\n \"okta\": {\n \"client_id\": os.environ.get(\"OKTA_CLIENT_ID\", \"\"),\n \"client_secret\": os.environ.get(\"OKTA_CLIENT_SECRET\", \"\"),\n \"server_metadata_url\": os.environ.get(\"OKTA_METADATA_URL\", \"\"),\n \"client_kwargs\": {\"scope\": \"openid email profile\"},\n },\n }\n\n # SAML 2.0 IdP configuration (PLT-030)\n # Set SAML_IDP_SSO_URL + SAML_SP_ENTITY_ID to enable SAML alongside OIDC.\n # Supported IdPs: ADFS, PingFederate, Shibboleth, Okta (SAML), Azure AD (SAML).\n # Routes registered at /account/saml/login, /account/saml/acs, /account/saml/metadata.\n SAML_IDP_SSO_URL = os.environ.get(\"SAML_IDP_SSO_URL\", \"\")\n # IdP Entity ID / Issuer URI (validated against Assertion Issuer element)\n SAML_IDP_ENTITY_ID = os.environ.get(\"SAML_IDP_ENTITY_ID\", \"\")\n # IdP X.509 signing certificate — PEM body without -----BEGIN/END----- headers.\n # Required for production signature validation (via xmlsec / python3-saml).\n SAML_IDP_CERT = os.environ.get(\"SAML_IDP_CERT\", \"\")\n # SP Entity ID — typically the platform's base URL\n SAML_SP_ENTITY_ID = os.environ.get(\"SAML_SP_ENTITY_ID\", \"\")\n # SP ACS URL — leave empty to auto-derive from url_for('account.saml_acs')\n SAML_SP_ACS_URL = os.environ.get(\"SAML_SP_ACS_URL\", \"\")\n\n # Internationalization (S2-01) — date/number/currency formatting\n # Full string translation (gettext) is Phase 2.\n BABEL_DEFAULT_LOCALE = os.environ.get(\"BABEL_DEFAULT_LOCALE\", \"en\")\n BABEL_DEFAULT_TIMEZONE = os.environ.get(\"BABEL_DEFAULT_TIMEZONE\", \"UTC\")\n\n # Read Replica Configuration (ENH-012)\n # Set DATABASE_READ_REPLICA_URL to a replica DB connection string to offload\n # dashboard and portfolio GET queries from the primary. When not configured,\n # all queries use the default SQLALCHEMY_DATABASE_URI (no-op).\n DATABASE_READ_REPLICA_URL = os.environ.get(\"DATABASE_READ_REPLICA_URL\", \"\")\n USE_READ_REPLICA_FOR_DASHBOARD = (\n os.environ.get(\"USE_READ_REPLICA_FOR_DASHBOARD\", \"false\").lower() == \"true\"\n )\n\n # AI-originated CRUD approval gate (A95-008)\n # When true, AI CRUD endpoints return 202 pending_approval instead of writing directly.\n REQUIRE_AI_APPROVAL = os.environ.get(\"REQUIRE_AI_APPROVAL\", \"false\").lower() == \"true\"\n\n # North Star Navigation (NORTH-STAR-001)\n # Enterprise-grade navigation for Fortune 500 TOGAF/ArchiMate practitioners.\n # When enabled: renamed navigation items, persona-based filtering, hidden admin items.\n # Rollback: set to false and restart service (zero risk, instant revert).\n # ENABLED 2026-04-02: Quick wins from NORTH_STAR_ASSESSMENT.md Part 10\n ENABLE_NORTH_STAR_NAV = _env_bool(\"ENABLE_NORTH_STAR_NAV\", True)\n\n # North Star Navigation Phase 2 (NORTH-STAR-002)\n # Complete ArchiMate 3.2 layer-based navigation with all 55 element types.\n # When enabled: 6-section IA, layer-first architecture, Portfolio/Architecture separation.\n # Depends on: ENABLE_NORTH_STAR_NAV=True (Phase 1 must be active)\n # Rollback: set to false and restart service.\n ENABLE_NORTH_STAR_PHASE2 = _env_bool(\"ENABLE_NORTH_STAR_PHASE2\", False)\n\n # Page-aware AI guide is fail-closed by default and only activates when\n # this flag is explicitly enabled and an LLM provider is configured.\n AI_PAGE_GUIDE_ENABLED = _env_bool(\"AI_PAGE_GUIDE_ENABLED\", False)\n\n # File Upload Settings\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max file size\n ALLOWED_EXTENSIONS = {\n \"pdf\",\n \"docx\",\n \"doc\",\n \"xlsx\",\n \"xls\",\n \"png\",\n \"jpg\",\n \"jpeg\",\n \"vsdx\",\n \"txt\",\n \"csv\",\n \"md\",\n }\n UPLOAD_FOLDER = os.path.join(basedir, \"uploads\", \"documents\")\n\n @staticmethod\n def init_app(app):\n \"\"\"Base class hook — subclasses override to customize app initialization.\"\"\"\n # Behind a TLS-terminating reverse proxy (Caddy/nginx/Traefik), trust the\n # X-Forwarded-* headers so request.scheme/host reflect the real https URL.\n # Without this the app builds http:// URLs behind https, causing\n # mixed-content blocking. Opt-in via TRUST_PROXY so forged headers are not\n # trusted when the app is exposed directly.\n if os.environ.get(\"TRUST_PROXY\", \"false\").lower() in (\"1\", \"true\", \"yes\") \\\n and not getattr(app, \"_proxyfix_applied\", False):\n from werkzeug.middleware.proxy_fix import ProxyFix\n app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)\n app._proxyfix_applied = True\n return\n\n\nclass DevelopmentConfig(Config):\n DEBUG = True\n ASSETS_DEBUG = True\n\n # N+1 query detection — set SQLALCHEMY_ECHO=true in .env to enable query logging\n SQLALCHEMY_ECHO = os.environ.get(\"SQLALCHEMY_ECHO\", \"false\").lower() == \"true\"\n JIRA_AUTO_PUSH = os.environ.get(\"JIRA_AUTO_PUSH\", \"false\").lower() == \"true\"\n\n # PostgreSQL configuration (REQUIRED - validated at runtime)\n # Default to a local Postgres instance for development when DATABASE_URL is not set.\n # Standard port 5432; set DATABASE_URL in .env to override (e.g. Windows installs\n # that run Postgres on 5439).\n SQLALCHEMY_DATABASE_URI = os.environ.get(\n \"DATABASE_URL\",\n \"postgresql://postgres:postgres@127.0.0.1:5432/archie\", # secrets-safety-ok\n )\n\n # SERVER_NAME must NOT be hardcoded — Flask-WTF CSRF validates the Referer\n # header against SERVER_NAME, so a hardcoded value causes \"CSRF token is missing\"\n # when the browser omits the Referer header (privacy settings, extensions, etc.).\n # Only set when explicitly provided via environment variable.\n SERVER_NAME = os.environ.get(\"SERVER_NAME\") or None\n PREFERRED_URL_SCHEME = os.environ.get(\"PREFERRED_URL_SCHEME\", \"http\")\n APPLICATION_ROOT = os.environ.get(\"APPLICATION_ROOT\", \"/\")\n\n # PostgreSQL connection options (optimized for local development)\n SQLALCHEMY_ENGINE_OPTIONS = {\n \"pool_pre_ping\": True,\n \"pool_recycle\": 300,\n \"pool_size\": 5,\n \"max_overflow\": 10,\n \"pool_timeout\": 30,\n \"connect_args\": {\n \"client_encoding\": \"utf8\",\n \"options\": \"-c client_encoding=utf8\",\n },\n \"pool_reset_on_return\": \"commit\",\n }\n\n @classmethod\n def init_app(cls, app):\n # Ensure a sensible default exists for development (Postgres is the standard)\n if not app.config.get(\"SQLALCHEMY_DATABASE_URI\"):\n app.logger.warning(\n \"No DATABASE_URL provided; falling back to local Postgres default.\"\n )\n\n\nclass TestingConfig(Config):\n TESTING = True\n WTF_CSRF_ENABLED = False\n\n # PostgreSQL REQUIRED for tests (matches production behavior)\n # SQLite is NOT supported - tests must use PostgreSQL for consistency\n SQLALCHEMY_DATABASE_URI = os.environ.get(\n \"TEST_DATABASE_URL\",\n \"postgresql://postgres:postgres@127.0.0.1:5432/archie_test\", # secrets-safety-ok\n )\n\n # PostgreSQL connection options for testing\n SQLALCHEMY_ENGINE_OPTIONS = {\n \"pool_pre_ping\": True,\n \"pool_size\": 5,\n \"max_overflow\": 10,\n }\n\n @classmethod\n def init_app(cls, app):\n # Validate PostgreSQL is being used (not SQLite)\n db_uri = app.config.get(\"SQLALCHEMY_DATABASE_URI\", \"\")\n if \"sqlite\" in db_uri.lower():\n raise ValueError(\n \"TEST_DATABASE_URL must be PostgreSQL, not SQLite. \"\n \"Set TEST_DATABASE_URL to a PostgreSQL connection string. \"\n \"Example: postgresql://postgres:postgres@localhost:5432/flask_test\" # secrets-safety-ok\n )\n\n if not db_uri:\n app.logger.warning(\n \"No TEST_DATABASE_URL provided; using default local PostgreSQL test database.\"\n )\n\n print(\"THIS APP IS IN TESTING MODE. YOU SHOULD NOT SEE THIS IN PRODUCTION.\")\n\n\nclass ProductionConfig(Config):\n DEBUG = False\n USE_RELOADER = False\n\n # Session cookie security\n _cookie_secure_default = os.environ.get(\"PREFERRED_URL_SCHEME\", \"http\").lower() == \"https\"\n SESSION_COOKIE_SECURE = _env_bool(\"SESSION_COOKIE_SECURE\", _cookie_secure_default)\n SESSION_COOKIE_HTTPONLY = True\n SESSION_COOKIE_SAMESITE = \"Lax\"\n REMEMBER_COOKIE_SECURE = _env_bool(\"REMEMBER_COOKIE_SECURE\", _cookie_secure_default)\n REMEMBER_COOKIE_HTTPONLY = True\n\n # PostgreSQL for production (REQUIRED - validated at runtime)\n SQLALCHEMY_DATABASE_URI = os.environ.get(\"DATABASE_URL\")\n SSL_DISABLE = os.environ.get(\"SSL_DISABLE\", \"False\") == \"True\"\n JIRA_AUTO_PUSH = os.environ.get(\"JIRA_AUTO_PUSH\", \"false\").lower() == \"true\"\n\n # PostgreSQL connection options (optimized for production)\n # Sized per-process, not per-deployment: preload_app=True plus the\n # post_fork db.engine.dispose() hook means every gunicorn worker and the\n # RQ worker gets its own full pool. The hardcoded 20+30 let 2 workers +\n # RQ worker demand 150 connections against max_connections=100. Env-\n # overridable so the pool can track GUNICORN_THREADS without a code change.\n SQLALCHEMY_ENGINE_OPTIONS = {\n \"pool_pre_ping\": True,\n \"pool_recycle\": 300,\n \"pool_size\": int(os.environ.get(\"SQLALCHEMY_POOL_SIZE\", 20)),\n \"max_overflow\": int(os.environ.get(\"SQLALCHEMY_MAX_OVERFLOW\", 30)),\n \"pool_timeout\": 30,\n \"connect_args\": {\n \"client_encoding\": \"utf8\",\n \"options\": \"-c client_encoding=utf8\",\n },\n \"pool_reset_on_return\": \"commit\",\n }\n\n @classmethod\n def init_app(cls, app):\n # Validate DATABASE_URL is set\n if not app.config.get(\"SQLALCHEMY_DATABASE_URI\"):\n raise ValueError(\n \"DATABASE_URL environment variable is required. PostgreSQL must be configured for production.\"\n )\n\n Config.init_app(app)\n assert os.environ.get(\"SECRET_KEY\"), \"SECRET_KEY IS NOT SET!\"\n\n # Trust the nginx reverse proxy's forwarded headers (X-Forwarded-Proto /\n # -For) so request.scheme/host reflect the real client-facing protocol.\n # Without this, behind TLS the app still builds http:// external URLs\n # (breaking OAuth redirect URIs, email links) and treats requests as\n # insecure. nginx is the single trusted hop. Idempotent so a subclass\n # (Heroku/Unix) re-running init_app cannot double-wrap.\n if not getattr(app, \"_proxyfix_applied\", False):\n from werkzeug.middleware.proxy_fix import ProxyFix\n\n app.wsgi_app = ProxyFix(\n app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1\n )\n app._proxyfix_applied = True\n\n if HAS_RAYGUN and flask_raygun and app.config.get(\"RAYGUN_APIKEY\"):\n flask_raygun.Provider(app, app.config[\"RAYGUN_APIKEY\"]).attach()\n\n\nclass HerokuConfig(ProductionConfig):\n @classmethod\n def init_app(cls, app):\n # ProductionConfig.init_app applies ProxyFix (idempotently); no need to\n # re-wrap here.\n ProductionConfig.init_app(app)\n\n\nclass UnixConfig(ProductionConfig):\n @classmethod\n def init_app(cls, app):\n ProductionConfig.init_app(app)\n\n # Log to syslog\n import logging\n from logging.handlers import SysLogHandler\n\n syslog_handler = SysLogHandler()\n syslog_handler.setLevel(logging.WARNING)\n app.logger.addHandler(syslog_handler)\n\n\n# Currency Configuration System\nclass CurrencyConfig:\n \"\"\"Centralized currency configuration for the application\"\"\"\n\n DEFAULT_CURRENCY = os.environ.get(\"DEFAULT_CURRENCY\", \"GBP\")\n\n SUPPORTED_CURRENCIES = {\n \"GBP\": {\n \"symbol\": \"£\",\n \"code\": \"GBP\",\n \"position\": \"prefix\", # prefix or suffix\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"British Pound Sterling\",\n },\n \"USD\": {\n \"symbol\": \"$\",\n \"code\": \"USD\",\n \"position\": \"prefix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"United States Dollar\",\n },\n \"EUR\": {\n \"symbol\": \"€\",\n \"code\": \"EUR\",\n \"position\": \"suffix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \".\",\n \"name\": \"Euro\",\n },\n \"JPY\": {\n \"symbol\": \"¥\",\n \"code\": \"JPY\",\n \"position\": \"prefix\",\n \"decimal_places\": 0,\n \"thousands_separator\": \",\",\n \"name\": \"Japanese Yen\",\n },\n \"CAD\": {\n \"symbol\": \"C$\",\n \"code\": \"CAD\",\n \"position\": \"prefix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"Canadian Dollar\",\n },\n \"AUD\": {\n \"symbol\": \"A$\",\n \"code\": \"AUD\",\n \"position\": \"prefix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"Australian Dollar\",\n },\n }\n\n @classmethod\n def get_currency_config(cls, currency_code=None):\n \"\"\"Get currency configuration for specified currency code\"\"\"\n if not currency_code:\n currency_code = cls.DEFAULT_CURRENCY\n\n config = cls.SUPPORTED_CURRENCIES.get(currency_code)\n if not config:\n # Fallback to default currency instead of crashing\n config = cls.SUPPORTED_CURRENCIES.get(cls.DEFAULT_CURRENCY)\n\n return config\n\n @classmethod\n def is_supported(cls, currency_code):\n \"\"\"Check if currency is supported\"\"\"\n return currency_code in cls.SUPPORTED_CURRENCIES\n\n @classmethod\n def get_all_supported_codes(cls):\n \"\"\"Get list of all supported currency codes\"\"\"\n return list(cls.SUPPORTED_CURRENCIES.keys())\n\n\nconfig = {\n \"development\": DevelopmentConfig,\n \"testing\": TestingConfig,\n \"production\": ProductionConfig,\n \"default\": DevelopmentConfig,\n \"heroku\": HerokuConfig,\n \"unix\": UnixConfig,\n}\n"}, "9493e69d966587b7f8b4ef3f40535c797fe31b46": {"t": 1785753431.8356373, "n": 21096, "h": "11e3d14837367662", "c": "import os\nfrom datetime import timedelta\n\n# Optional raygun import\ntry:\n from raygun4py.middleware import flask as flask_raygun\n\n HAS_RAYGUN = True\nexcept ImportError:\n HAS_RAYGUN = False\n flask_raygun = None\n\n# Python 3 only (Python 2 is no longer supported)\nimport urllib.parse\n\nbasedir = os.path.abspath(os.path.dirname(__file__))\n\n# Canonical module configuration. The v2 (\"guardrail\") modules are the active code\n# path (see CLAUDE.md), and the navigation/templates reference their endpoints, so\n# they MUST be registered by default. Without these flags a fresh install registers\n# only legacy/partial blueprints and every authenticated page 500s on a sidebar\n# url_for to an unregistered endpoint (e.g. solution_design.list_solutions). Using\n# setdefault means an explicit env value (true/false) is still respected.\nfor _canonical_flag in (\n \"USE_SOLUTIONS_STRATEGIC_GUARDRAILS\",\n \"USE_NEW_SOLUTIONS_STRATEGIC\",\n \"USE_CAPABILITIES_GUARDRAILS\",\n \"USE_NEW_CAPABILITIES\",\n \"USE_DASHBOARD_GUARDRAILS\",\n \"USE_NEW_DASHBOARD\",\n \"USE_APPLICATIONS_GUARDRAILS\",\n \"USE_NEW_APPLICATIONS\",\n \"USE_ARCHITECTURE_GUARDRAILS\",\n \"USE_NEW_ARCHITECTURE\",\n \"USE_AI_CHAT_GUARDRAILS\",\n \"USE_NEW_AI_CHAT\",\n # NOTE: vendors are intentionally NOT defaulted to the v2/guardrail path.\n # The always-on unified vendor blueprints serve the vendor pages AND the\n # /api/vendors/* JSON the vendor dashboard widgets call (list, ranking).\n # Forcing USE_VENDORS_GUARDRAILS switches to v2-only and drops the unified\n # API, 404-ing those widgets (\"not found\" in the UI).\n):\n os.environ.setdefault(_canonical_flag, \"true\")\n\n\ndef _env_bool(name: str, default: bool) -> bool:\n value = os.environ.get(name)\n if value is None:\n return default\n return value.strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\n\n# Load environment variables from .env file\nfrom dotenv import load_dotenv\n\n# Load from .env file using explicit path (ensures it works regardless of working directory)\nenv_path = os.path.join(basedir, \".env\")\nif os.path.exists(env_path):\n load_dotenv(env_path)\n print(f\"Loaded environment from: {env_path}\")\nelse:\n # Fallback to default behavior\n load_dotenv()\n print(\"Loaded environment from default location\")\n\n# Verify critical API keys are loaded\n_api_keys_loaded = []\nfor key in [\n \"OPENAI_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"GEMINI_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"HUGGINGFACE_API_KEY\",\n]:\n if os.getenv(key):\n _api_keys_loaded.append(key)\nif _api_keys_loaded:\n print(f\"API keys loaded: {', '.join(_api_keys_loaded)}\")\nelse:\n print(\"WARNING: No LLM API keys found in environment!\")\n\n\nclass Config:\n APP_NAME = os.environ.get(\"APP_NAME\", \"A.R.C.H.I.E.\")\n SECRET_KEY = os.environ.get(\"SECRET_KEY\")\n if not SECRET_KEY:\n import secrets\n\n SECRET_KEY = secrets.token_hex(32)\n print(\n \"WARNING: SECRET_KEY env var not set. A random key has been generated \"\n \"for this session. Sessions will NOT persist across restarts. \"\n \"Set SECRET_KEY in your .env file for production use.\"\n )\n SQLALCHEMY_COMMIT_ON_TEARDOWN = True\n\n # Session security — 8-hour session lifetime, 30-day remember-me cookie\n PERMANENT_SESSION_LIFETIME = timedelta(hours=8)\n REMEMBER_COOKIE_DURATION = timedelta(days=30)\n SESSION_REFRESH_EACH_REQUEST = True # FAR-012: Ensure session is refreshed on every request\n REMEMBER_COOKIE_REFRESH_EACH_REQUEST = True # FAR-012: Refresh remember-me cookie on activity\n\n # JWT Configuration\n JWT_TOKEN_LOCATION = [\"headers\"]\n JWT_SECRET_KEY = os.environ.get(\"JWT_SECRET_KEY\") or SECRET_KEY\n JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1)\n\n # Email\n MAIL_SERVER = os.environ.get(\"MAIL_SERVER\", \"smtp.sendgrid.net\")\n MAIL_PORT = os.environ.get(\"MAIL_PORT\", 587)\n MAIL_USE_TLS = os.environ.get(\"MAIL_USE_TLS\", True)\n MAIL_USE_SSL = os.environ.get(\"MAIL_USE_SSL\", False)\n MAIL_USERNAME = os.environ.get(\"MAIL_USERNAME\")\n MAIL_PASSWORD = os.environ.get(\"MAIL_PASSWORD\")\n MAIL_DEFAULT_SENDER = os.environ.get(\"MAIL_DEFAULT_SENDER\")\n\n # Analytics\n GOOGLE_ANALYTICS_ID = os.environ.get(\"GOOGLE_ANALYTICS_ID\", \"\")\n SEGMENT_API_KEY = os.environ.get(\"SEGMENT_API_KEY\", \"\")\n\n # Admin account\n ADMIN_PASSWORD = os.environ.get(\"ADMIN_PASSWORD\")\n if not ADMIN_PASSWORD:\n import secrets\n\n ADMIN_PASSWORD = secrets.token_urlsafe(16)\n print(\n \"WARNING: ADMIN_PASSWORD env var not set. Generated random password. \"\n \"Set ADMIN_PASSWORD in your .env file for production use.\"\n )\n ADMIN_EMAIL = os.environ.get(\"ADMIN_EMAIL\", \"flask-base-admin@example.com\")\n EMAIL_SUBJECT_PREFIX = \"[{}]\".format(APP_NAME)\n EMAIL_SENDER = \"{app_name} Admin <{email}>\".format(\n app_name=APP_NAME, email=MAIL_USERNAME\n )\n\n # ARCHIE Deploy: Credential encryption + Coolify PaaS + n8n connector sync\n CREDENTIAL_ENCRYPTION_KEY = os.environ.get(\"CREDENTIAL_ENCRYPTION_KEY\", \"\")\n COOLIFY_API_URL = os.environ.get(\"COOLIFY_API_URL\", \"http://localhost:8000\")\n COOLIFY_API_TOKEN = os.environ.get(\"COOLIFY_API_TOKEN\", \"\")\n COOLIFY_DOMAIN_SUFFIX = os.environ.get(\"COOLIFY_DOMAIN_SUFFIX\", \"archie.example.com\")\n N8N_API_URL = os.environ.get(\"N8N_API_URL\", \"http://localhost:5678\")\n N8N_API_TOKEN = os.environ.get(\"N8N_API_TOKEN\", \"\")\n\n REDIS_URL = os.getenv(\"REDIS_URL\") or os.getenv(\n \"REDISTOGO_URL\", \"redis://localhost:6379/0\"\n )\n\n RAYGUN_APIKEY = os.environ.get(\"RAYGUN_APIKEY\")\n\n # Stripe Billing (COM-001) — platform works without these; set in production .env\n STRIPE_SECRET_KEY = os.environ.get(\"STRIPE_SECRET_KEY\", \"\")\n STRIPE_WEBHOOK_SECRET = os.environ.get(\"STRIPE_WEBHOOK_SECRET\", \"\")\n STRIPE_PRICE_PRO = os.environ.get(\"STRIPE_PRICE_PRO\", \"\")\n STRIPE_PRICE_ENTERPRISE = os.environ.get(\"STRIPE_PRICE_ENTERPRISE\", \"\")\n\n # Parse the REDIS_URL to set RQ config variables\n urllib.parse.uses_netloc.append(\"redis\")\n url = urllib.parse.urlparse(REDIS_URL)\n\n RQ_DEFAULT_HOST = url.hostname\n RQ_DEFAULT_PORT = url.port\n RQ_DEFAULT_PASSWORD = url.password\n RQ_DEFAULT_DB = 0\n\n # Celery configuration (async task queue)\n # Feature-flagged: set CELERY_ENABLED=true to enable async batch import\n CELERY_ENABLED = os.environ.get(\"CELERY_ENABLED\", \"false\").lower() == \"true\"\n CELERY_BROKER_URL = os.environ.get(\"CELERY_BROKER_URL\", REDIS_URL)\n CELERY_RESULT_BACKEND = os.environ.get(\"CELERY_RESULT_BACKEND\", REDIS_URL)\n CELERY_TASK_SERIALIZER = \"json\"\n CELERY_ACCEPT_CONTENT = [\"json\"]\n CELERY_RESULT_SERIALIZER = \"json\"\n CELERY_TIMEZONE = \"UTC\"\n CELERY_ENABLE_UTC = True\n\n # SSO / Enterprise Identity (S0-01)\n # Supports Azure AD, Okta (OIDC) and any SAML 2.0 IdP (PLT-030).\n # Feature-flagged: SSO routes only active when FeatureFlag(key='sso_authentication') is enabled.\n SSO_PROVIDERS = {\n \"azure\": {\n \"client_id\": os.environ.get(\"AZURE_AD_CLIENT_ID\", \"\"),\n \"client_secret\": os.environ.get(\"AZURE_AD_CLIENT_SECRET\", \"\"),\n \"server_metadata_url\": os.environ.get(\n \"AZURE_AD_METADATA_URL\",\n \"https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration\".format(\n tenant=os.environ.get(\"AZURE_AD_TENANT_ID\", \"common\")\n ),\n ),\n \"client_kwargs\": {\"scope\": \"openid email profile\"},\n },\n \"okta\": {\n \"client_id\": os.environ.get(\"OKTA_CLIENT_ID\", \"\"),\n \"client_secret\": os.environ.get(\"OKTA_CLIENT_SECRET\", \"\"),\n \"server_metadata_url\": os.environ.get(\"OKTA_METADATA_URL\", \"\"),\n \"client_kwargs\": {\"scope\": \"openid email profile\"},\n },\n }\n\n # SAML 2.0 IdP configuration (PLT-030)\n # Set SAML_IDP_SSO_URL + SAML_SP_ENTITY_ID to enable SAML alongside OIDC.\n # Supported IdPs: ADFS, PingFederate, Shibboleth, Okta (SAML), Azure AD (SAML).\n # Routes registered at /account/saml/login, /account/saml/acs, /account/saml/metadata.\n SAML_IDP_SSO_URL = os.environ.get(\"SAML_IDP_SSO_URL\", \"\")\n # IdP Entity ID / Issuer URI (validated against Assertion Issuer element)\n SAML_IDP_ENTITY_ID = os.environ.get(\"SAML_IDP_ENTITY_ID\", \"\")\n # IdP X.509 signing certificate — PEM body without -----BEGIN/END----- headers.\n # Required for production signature validation (via xmlsec / python3-saml).\n SAML_IDP_CERT = os.environ.get(\"SAML_IDP_CERT\", \"\")\n # SP Entity ID — typically the platform's base URL\n SAML_SP_ENTITY_ID = os.environ.get(\"SAML_SP_ENTITY_ID\", \"\")\n # SP ACS URL — leave empty to auto-derive from url_for('account.saml_acs')\n SAML_SP_ACS_URL = os.environ.get(\"SAML_SP_ACS_URL\", \"\")\n\n # Internationalization (S2-01) — date/number/currency formatting\n # Full string translation (gettext) is Phase 2.\n BABEL_DEFAULT_LOCALE = os.environ.get(\"BABEL_DEFAULT_LOCALE\", \"en\")\n BABEL_DEFAULT_TIMEZONE = os.environ.get(\"BABEL_DEFAULT_TIMEZONE\", \"UTC\")\n\n # Read Replica Configuration (ENH-012)\n # Set DATABASE_READ_REPLICA_URL to a replica DB connection string to offload\n # dashboard and portfolio GET queries from the primary. When not configured,\n # all queries use the default SQLALCHEMY_DATABASE_URI (no-op).\n DATABASE_READ_REPLICA_URL = os.environ.get(\"DATABASE_READ_REPLICA_URL\", \"\")\n USE_READ_REPLICA_FOR_DASHBOARD = (\n os.environ.get(\"USE_READ_REPLICA_FOR_DASHBOARD\", \"false\").lower() == \"true\"\n )\n\n # AI-originated CRUD approval gate (A95-008)\n # When true, AI CRUD endpoints return 202 pending_approval instead of writing directly.\n REQUIRE_AI_APPROVAL = os.environ.get(\"REQUIRE_AI_APPROVAL\", \"false\").lower() == \"true\"\n\n # North Star Navigation (NORTH-STAR-001)\n # Enterprise-grade navigation for Fortune 500 TOGAF/ArchiMate practitioners.\n # When enabled: renamed navigation items, persona-based filtering, hidden admin items.\n # Rollback: set to false and restart service (zero risk, instant revert).\n # ENABLED 2026-04-02: Quick wins from NORTH_STAR_ASSESSMENT.md Part 10\n ENABLE_NORTH_STAR_NAV = _env_bool(\"ENABLE_NORTH_STAR_NAV\", True)\n\n # North Star Navigation Phase 2 (NORTH-STAR-002)\n # Complete ArchiMate 3.2 layer-based navigation with all 55 element types.\n # When enabled: 6-section IA, layer-first architecture, Portfolio/Architecture separation.\n # Depends on: ENABLE_NORTH_STAR_NAV=True (Phase 1 must be active)\n # Rollback: set to false and restart service.\n ENABLE_NORTH_STAR_PHASE2 = _env_bool(\"ENABLE_NORTH_STAR_PHASE2\", False)\n\n # Page-aware AI guide is fail-closed by default and only activates when\n # this flag is explicitly enabled and an LLM provider is configured.\n AI_PAGE_GUIDE_ENABLED = _env_bool(\"AI_PAGE_GUIDE_ENABLED\", False)\n\n # File Upload Settings\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max file size\n ALLOWED_EXTENSIONS = {\n \"pdf\",\n \"docx\",\n \"doc\",\n \"xlsx\",\n \"xls\",\n \"png\",\n \"jpg\",\n \"jpeg\",\n \"vsdx\",\n \"txt\",\n \"csv\",\n \"md\",\n }\n UPLOAD_FOLDER = os.path.join(basedir, \"uploads\", \"documents\")\n\n @staticmethod\n def init_app(app):\n \"\"\"Base class hook — subclasses override to customize app initialization.\"\"\"\n # Behind a TLS-terminating reverse proxy (Caddy/nginx/Traefik), trust the\n # X-Forwarded-* headers so request.scheme/host reflect the real https URL.\n # Without this the app builds http:// URLs behind https, causing\n # mixed-content blocking. Opt-in via TRUST_PROXY so forged headers are not\n # trusted when the app is exposed directly.\n if os.environ.get(\"TRUST_PROXY\", \"false\").lower() in (\"1\", \"true\", \"yes\") \\\n and not getattr(app, \"_proxyfix_applied\", False):\n from werkzeug.middleware.proxy_fix import ProxyFix\n app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)\n app._proxyfix_applied = True\n return\n\n\nclass DevelopmentConfig(Config):\n DEBUG = True\n ASSETS_DEBUG = True\n\n # N+1 query detection — set SQLALCHEMY_ECHO=true in .env to enable query logging\n SQLALCHEMY_ECHO = os.environ.get(\"SQLALCHEMY_ECHO\", \"false\").lower() == \"true\"\n JIRA_AUTO_PUSH = os.environ.get(\"JIRA_AUTO_PUSH\", \"false\").lower() == \"true\"\n\n # PostgreSQL configuration (REQUIRED - validated at runtime)\n # Default to a local Postgres instance for development when DATABASE_URL is not set.\n # Standard port 5432; set DATABASE_URL in .env to override (e.g. Windows installs\n # that run Postgres on 5439).\n SQLALCHEMY_DATABASE_URI = os.environ.get(\n \"DATABASE_URL\",\n \"postgresql://postgres:postgres@127.0.0.1:5432/archie\", # secrets-safety-ok\n )\n\n # SERVER_NAME must NOT be hardcoded — Flask-WTF CSRF validates the Referer\n # header against SERVER_NAME, so a hardcoded value causes \"CSRF token is missing\"\n # when the browser omits the Referer header (privacy settings, extensions, etc.).\n # Only set when explicitly provided via environment variable.\n SERVER_NAME = os.environ.get(\"SERVER_NAME\") or None\n PREFERRED_URL_SCHEME = os.environ.get(\"PREFERRED_URL_SCHEME\", \"http\")\n APPLICATION_ROOT = os.environ.get(\"APPLICATION_ROOT\", \"/\")\n\n # PostgreSQL connection options (optimized for local development)\n SQLALCHEMY_ENGINE_OPTIONS = {\n \"pool_pre_ping\": True,\n \"pool_recycle\": 300,\n \"pool_size\": 5,\n \"max_overflow\": 10,\n \"pool_timeout\": 30,\n \"connect_args\": {\n \"client_encoding\": \"utf8\",\n \"options\": \"-c client_encoding=utf8\",\n },\n \"pool_reset_on_return\": \"commit\",\n }\n\n @classmethod\n def init_app(cls, app):\n # Ensure a sensible default exists for development (Postgres is the standard)\n if not app.config.get(\"SQLALCHEMY_DATABASE_URI\"):\n app.logger.warning(\n \"No DATABASE_URL provided; falling back to local Postgres default.\"\n )\n\n\nclass TestingConfig(Config):\n TESTING = True\n WTF_CSRF_ENABLED = False\n\n # PostgreSQL REQUIRED for tests (matches production behavior)\n # SQLite is NOT supported - tests must use PostgreSQL for consistency\n SQLALCHEMY_DATABASE_URI = os.environ.get(\n \"TEST_DATABASE_URL\",\n \"postgresql://postgres:postgres@127.0.0.1:5432/archie_test\", # secrets-safety-ok\n )\n\n # PostgreSQL connection options for testing\n SQLALCHEMY_ENGINE_OPTIONS = {\n \"pool_pre_ping\": True,\n \"pool_size\": 5,\n \"max_overflow\": 10,\n }\n\n @classmethod\n def init_app(cls, app):\n # Validate PostgreSQL is being used (not SQLite)\n db_uri = app.config.get(\"SQLALCHEMY_DATABASE_URI\", \"\")\n if \"sqlite\" in db_uri.lower():\n raise ValueError(\n \"TEST_DATABASE_URL must be PostgreSQL, not SQLite. \"\n \"Set TEST_DATABASE_URL to a PostgreSQL connection string. \"\n \"Example: postgresql://postgres:postgres@localhost:5432/flask_test\" # secrets-safety-ok\n )\n\n if not db_uri:\n app.logger.warning(\n \"No TEST_DATABASE_URL provided; using default local PostgreSQL test database.\"\n )\n\n print(\"THIS APP IS IN TESTING MODE. YOU SHOULD NOT SEE THIS IN PRODUCTION.\")\n\n\nclass ProductionConfig(Config):\n DEBUG = False\n USE_RELOADER = False\n\n # Session cookie security\n _cookie_secure_default = os.environ.get(\"PREFERRED_URL_SCHEME\", \"http\").lower() == \"https\"\n SESSION_COOKIE_SECURE = _env_bool(\"SESSION_COOKIE_SECURE\", _cookie_secure_default)\n SESSION_COOKIE_HTTPONLY = True\n SESSION_COOKIE_SAMESITE = \"Lax\"\n REMEMBER_COOKIE_SECURE = _env_bool(\"REMEMBER_COOKIE_SECURE\", _cookie_secure_default)\n REMEMBER_COOKIE_HTTPONLY = True\n\n # PostgreSQL for production (REQUIRED - validated at runtime)\n SQLALCHEMY_DATABASE_URI = os.environ.get(\"DATABASE_URL\")\n SSL_DISABLE = os.environ.get(\"SSL_DISABLE\", \"False\") == \"True\"\n JIRA_AUTO_PUSH = os.environ.get(\"JIRA_AUTO_PUSH\", \"false\").lower() == \"true\"\n\n # PostgreSQL connection options (optimized for production)\n SQLALCHEMY_ENGINE_OPTIONS = {\n \"pool_pre_ping\": True,\n \"pool_recycle\": 300,\n \"pool_size\": 20,\n \"max_overflow\": 30,\n \"pool_timeout\": 30,\n \"connect_args\": {\n \"client_encoding\": \"utf8\",\n \"options\": \"-c client_encoding=utf8\",\n },\n \"pool_reset_on_return\": \"commit\",\n }\n\n @classmethod\n def init_app(cls, app):\n # Validate DATABASE_URL is set\n if not app.config.get(\"SQLALCHEMY_DATABASE_URI\"):\n raise ValueError(\n \"DATABASE_URL environment variable is required. PostgreSQL must be configured for production.\"\n )\n\n Config.init_app(app)\n assert os.environ.get(\"SECRET_KEY\"), \"SECRET_KEY IS NOT SET!\"\n\n # Trust the nginx reverse proxy's forwarded headers (X-Forwarded-Proto /\n # -For) so request.scheme/host reflect the real client-facing protocol.\n # Without this, behind TLS the app still builds http:// external URLs\n # (breaking OAuth redirect URIs, email links) and treats requests as\n # insecure. nginx is the single trusted hop. Idempotent so a subclass\n # (Heroku/Unix) re-running init_app cannot double-wrap.\n if not getattr(app, \"_proxyfix_applied\", False):\n from werkzeug.middleware.proxy_fix import ProxyFix\n\n app.wsgi_app = ProxyFix(\n app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1\n )\n app._proxyfix_applied = True\n\n if HAS_RAYGUN and flask_raygun and app.config.get(\"RAYGUN_APIKEY\"):\n flask_raygun.Provider(app, app.config[\"RAYGUN_APIKEY\"]).attach()\n\n\nclass HerokuConfig(ProductionConfig):\n @classmethod\n def init_app(cls, app):\n # ProductionConfig.init_app applies ProxyFix (idempotently); no need to\n # re-wrap here.\n ProductionConfig.init_app(app)\n\n\nclass UnixConfig(ProductionConfig):\n @classmethod\n def init_app(cls, app):\n ProductionConfig.init_app(app)\n\n # Log to syslog\n import logging\n from logging.handlers import SysLogHandler\n\n syslog_handler = SysLogHandler()\n syslog_handler.setLevel(logging.WARNING)\n app.logger.addHandler(syslog_handler)\n\n\n# Currency Configuration System\nclass CurrencyConfig:\n \"\"\"Centralized currency configuration for the application\"\"\"\n\n DEFAULT_CURRENCY = os.environ.get(\"DEFAULT_CURRENCY\", \"GBP\")\n\n SUPPORTED_CURRENCIES = {\n \"GBP\": {\n \"symbol\": \"£\",\n \"code\": \"GBP\",\n \"position\": \"prefix\", # prefix or suffix\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"British Pound Sterling\",\n },\n \"USD\": {\n \"symbol\": \"$\",\n \"code\": \"USD\",\n \"position\": \"prefix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"United States Dollar\",\n },\n \"EUR\": {\n \"symbol\": \"€\",\n \"code\": \"EUR\",\n \"position\": \"suffix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \".\",\n \"name\": \"Euro\",\n },\n \"JPY\": {\n \"symbol\": \"¥\",\n \"code\": \"JPY\",\n \"position\": \"prefix\",\n \"decimal_places\": 0,\n \"thousands_separator\": \",\",\n \"name\": \"Japanese Yen\",\n },\n \"CAD\": {\n \"symbol\": \"C$\",\n \"code\": \"CAD\",\n \"position\": \"prefix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"Canadian Dollar\",\n },\n \"AUD\": {\n \"symbol\": \"A$\",\n \"code\": \"AUD\",\n \"position\": \"prefix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"Australian Dollar\",\n },\n }\n\n @classmethod\n def get_currency_config(cls, currency_code=None):\n \"\"\"Get currency configuration for specified currency code\"\"\"\n if not currency_code:\n currency_code = cls.DEFAULT_CURRENCY\n\n config = cls.SUPPORTED_CURRENCIES.get(currency_code)\n if not config:\n # Fallback to default currency instead of crashing\n config = cls.SUPPORTED_CURRENCIES.get(cls.DEFAULT_CURRENCY)\n\n return config\n\n @classmethod\n def is_supported(cls, currency_code):\n \"\"\"Check if currency is supported\"\"\"\n return currency_code in cls.SUPPORTED_CURRENCIES\n\n @classmethod\n def get_all_supported_codes(cls):\n \"\"\"Get list of all supported currency codes\"\"\"\n return list(cls.SUPPORTED_CURRENCIES.keys())\n\n\nconfig = {\n \"development\": DevelopmentConfig,\n \"testing\": TestingConfig,\n \"production\": ProductionConfig,\n \"default\": DevelopmentConfig,\n \"heroku\": HerokuConfig,\n \"unix\": UnixConfig,\n}\n"}, "597c03acbd7898d5f47c4248081bcf1c1db27094": {"t": 1785753433.5703597, "n": 20377, "h": "dc659668737ec67c", "c": "import os\nfrom datetime import timedelta\n\n# Optional raygun import\ntry:\n from raygun4py.middleware import flask as flask_raygun\n\n HAS_RAYGUN = True\nexcept ImportError:\n HAS_RAYGUN = False\n flask_raygun = None\n\n# Python 3 only (Python 2 is no longer supported)\nimport urllib.parse\n\nbasedir = os.path.abspath(os.path.dirname(__file__))\n\n# Canonical module configuration. The v2 (\"guardrail\") modules are the active code\n# path (see CLAUDE.md), and the navigation/templates reference their endpoints, so\n# they MUST be registered by default. Without these flags a fresh install registers\n# only legacy/partial blueprints and every authenticated page 500s on a sidebar\n# url_for to an unregistered endpoint (e.g. solution_design.list_solutions). Using\n# setdefault means an explicit env value (true/false) is still respected.\nfor _canonical_flag in (\n \"USE_SOLUTIONS_STRATEGIC_GUARDRAILS\",\n \"USE_NEW_SOLUTIONS_STRATEGIC\",\n \"USE_CAPABILITIES_GUARDRAILS\",\n \"USE_NEW_CAPABILITIES\",\n \"USE_DASHBOARD_GUARDRAILS\",\n \"USE_NEW_DASHBOARD\",\n \"USE_APPLICATIONS_GUARDRAILS\",\n \"USE_NEW_APPLICATIONS\",\n \"USE_ARCHITECTURE_GUARDRAILS\",\n \"USE_NEW_ARCHITECTURE\",\n \"USE_AI_CHAT_GUARDRAILS\",\n \"USE_NEW_AI_CHAT\",\n # NOTE: vendors are intentionally NOT defaulted to the v2/guardrail path.\n # The always-on unified vendor blueprints serve the vendor pages AND the\n # /api/vendors/* JSON the vendor dashboard widgets call (list, ranking).\n # Forcing USE_VENDORS_GUARDRAILS switches to v2-only and drops the unified\n # API, 404-ing those widgets (\"not found\" in the UI).\n):\n os.environ.setdefault(_canonical_flag, \"true\")\n\n\ndef _env_bool(name: str, default: bool) -> bool:\n value = os.environ.get(name)\n if value is None:\n return default\n return value.strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\n\n# Load environment variables from .env file\nfrom dotenv import load_dotenv\n\n# Load from .env file using explicit path (ensures it works regardless of working directory)\nenv_path = os.path.join(basedir, \".env\")\nif os.path.exists(env_path):\n load_dotenv(env_path)\n print(f\"Loaded environment from: {env_path}\")\nelse:\n # Fallback to default behavior\n load_dotenv()\n print(\"Loaded environment from default location\")\n\n# Verify critical API keys are loaded\n_api_keys_loaded = []\nfor key in [\n \"OPENAI_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"GEMINI_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"HUGGINGFACE_API_KEY\",\n]:\n if os.getenv(key):\n _api_keys_loaded.append(key)\nif _api_keys_loaded:\n print(f\"API keys loaded: {', '.join(_api_keys_loaded)}\")\nelse:\n print(\"WARNING: No LLM API keys found in environment!\")\n\n\nclass Config:\n APP_NAME = os.environ.get(\"APP_NAME\", \"A.R.C.H.I.E.\")\n SECRET_KEY = os.environ.get(\"SECRET_KEY\")\n if not SECRET_KEY:\n import secrets\n\n SECRET_KEY = secrets.token_hex(32)\n print(\n \"WARNING: SECRET_KEY env var not set. A random key has been generated \"\n \"for this session. Sessions will NOT persist across restarts. \"\n \"Set SECRET_KEY in your .env file for production use.\"\n )\n SQLALCHEMY_COMMIT_ON_TEARDOWN = True\n\n # Session security — 8-hour session lifetime, 30-day remember-me cookie\n PERMANENT_SESSION_LIFETIME = timedelta(hours=8)\n REMEMBER_COOKIE_DURATION = timedelta(days=30)\n SESSION_REFRESH_EACH_REQUEST = True # FAR-012: Ensure session is refreshed on every request\n REMEMBER_COOKIE_REFRESH_EACH_REQUEST = True # FAR-012: Refresh remember-me cookie on activity\n\n # JWT Configuration\n JWT_TOKEN_LOCATION = [\"headers\"]\n JWT_SECRET_KEY = os.environ.get(\"JWT_SECRET_KEY\") or SECRET_KEY\n JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1)\n\n # Email\n MAIL_SERVER = os.environ.get(\"MAIL_SERVER\", \"smtp.sendgrid.net\")\n MAIL_PORT = os.environ.get(\"MAIL_PORT\", 587)\n MAIL_USE_TLS = os.environ.get(\"MAIL_USE_TLS\", True)\n MAIL_USE_SSL = os.environ.get(\"MAIL_USE_SSL\", False)\n MAIL_USERNAME = os.environ.get(\"MAIL_USERNAME\")\n MAIL_PASSWORD = os.environ.get(\"MAIL_PASSWORD\")\n MAIL_DEFAULT_SENDER = os.environ.get(\"MAIL_DEFAULT_SENDER\")\n\n # Analytics\n GOOGLE_ANALYTICS_ID = os.environ.get(\"GOOGLE_ANALYTICS_ID\", \"\")\n SEGMENT_API_KEY = os.environ.get(\"SEGMENT_API_KEY\", \"\")\n\n # Admin account\n ADMIN_PASSWORD = os.environ.get(\"ADMIN_PASSWORD\")\n if not ADMIN_PASSWORD:\n import secrets\n\n ADMIN_PASSWORD = secrets.token_urlsafe(16)\n print(\n \"WARNING: ADMIN_PASSWORD env var not set. Generated random password. \"\n \"Set ADMIN_PASSWORD in your .env file for production use.\"\n )\n ADMIN_EMAIL = os.environ.get(\"ADMIN_EMAIL\", \"flask-base-admin@example.com\")\n EMAIL_SUBJECT_PREFIX = \"[{}]\".format(APP_NAME)\n EMAIL_SENDER = \"{app_name} Admin <{email}>\".format(\n app_name=APP_NAME, email=MAIL_USERNAME\n )\n\n # ARCHIE Deploy: Credential encryption + Coolify PaaS + n8n connector sync\n CREDENTIAL_ENCRYPTION_KEY = os.environ.get(\"CREDENTIAL_ENCRYPTION_KEY\", \"\")\n COOLIFY_API_URL = os.environ.get(\"COOLIFY_API_URL\", \"http://localhost:8000\")\n COOLIFY_API_TOKEN = os.environ.get(\"COOLIFY_API_TOKEN\", \"\")\n COOLIFY_DOMAIN_SUFFIX = os.environ.get(\"COOLIFY_DOMAIN_SUFFIX\", \"archie.example.com\")\n N8N_API_URL = os.environ.get(\"N8N_API_URL\", \"http://localhost:5678\")\n N8N_API_TOKEN = os.environ.get(\"N8N_API_TOKEN\", \"\")\n\n REDIS_URL = os.getenv(\"REDIS_URL\") or os.getenv(\n \"REDISTOGO_URL\", \"redis://localhost:6379/0\"\n )\n\n RAYGUN_APIKEY = os.environ.get(\"RAYGUN_APIKEY\")\n\n # Stripe Billing (COM-001) — platform works without these; set in production .env\n STRIPE_SECRET_KEY = os.environ.get(\"STRIPE_SECRET_KEY\", \"\")\n STRIPE_WEBHOOK_SECRET = os.environ.get(\"STRIPE_WEBHOOK_SECRET\", \"\")\n STRIPE_PRICE_PRO = os.environ.get(\"STRIPE_PRICE_PRO\", \"\")\n STRIPE_PRICE_ENTERPRISE = os.environ.get(\"STRIPE_PRICE_ENTERPRISE\", \"\")\n\n # Parse the REDIS_URL to set RQ config variables\n urllib.parse.uses_netloc.append(\"redis\")\n url = urllib.parse.urlparse(REDIS_URL)\n\n RQ_DEFAULT_HOST = url.hostname\n RQ_DEFAULT_PORT = url.port\n RQ_DEFAULT_PASSWORD = url.password\n RQ_DEFAULT_DB = 0\n\n # Celery configuration (async task queue)\n # Feature-flagged: set CELERY_ENABLED=true to enable async batch import\n CELERY_ENABLED = os.environ.get(\"CELERY_ENABLED\", \"false\").lower() == \"true\"\n CELERY_BROKER_URL = os.environ.get(\"CELERY_BROKER_URL\", REDIS_URL)\n CELERY_RESULT_BACKEND = os.environ.get(\"CELERY_RESULT_BACKEND\", REDIS_URL)\n CELERY_TASK_SERIALIZER = \"json\"\n CELERY_ACCEPT_CONTENT = [\"json\"]\n CELERY_RESULT_SERIALIZER = \"json\"\n CELERY_TIMEZONE = \"UTC\"\n CELERY_ENABLE_UTC = True\n\n # SSO / Enterprise Identity (S0-01)\n # Supports Azure AD, Okta (OIDC) and any SAML 2.0 IdP (PLT-030).\n # Feature-flagged: SSO routes only active when FeatureFlag(key='sso_authentication') is enabled.\n SSO_PROVIDERS = {\n \"azure\": {\n \"client_id\": os.environ.get(\"AZURE_AD_CLIENT_ID\", \"\"),\n \"client_secret\": os.environ.get(\"AZURE_AD_CLIENT_SECRET\", \"\"),\n \"server_metadata_url\": os.environ.get(\n \"AZURE_AD_METADATA_URL\",\n \"https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration\".format(\n tenant=os.environ.get(\"AZURE_AD_TENANT_ID\", \"common\")\n ),\n ),\n \"client_kwargs\": {\"scope\": \"openid email profile\"},\n },\n \"okta\": {\n \"client_id\": os.environ.get(\"OKTA_CLIENT_ID\", \"\"),\n \"client_secret\": os.environ.get(\"OKTA_CLIENT_SECRET\", \"\"),\n \"server_metadata_url\": os.environ.get(\"OKTA_METADATA_URL\", \"\"),\n \"client_kwargs\": {\"scope\": \"openid email profile\"},\n },\n }\n\n # SAML 2.0 IdP configuration (PLT-030)\n # Set SAML_IDP_SSO_URL + SAML_SP_ENTITY_ID to enable SAML alongside OIDC.\n # Supported IdPs: ADFS, PingFederate, Shibboleth, Okta (SAML), Azure AD (SAML).\n # Routes registered at /account/saml/login, /account/saml/acs, /account/saml/metadata.\n SAML_IDP_SSO_URL = os.environ.get(\"SAML_IDP_SSO_URL\", \"\")\n # IdP Entity ID / Issuer URI (validated against Assertion Issuer element)\n SAML_IDP_ENTITY_ID = os.environ.get(\"SAML_IDP_ENTITY_ID\", \"\")\n # IdP X.509 signing certificate — PEM body without -----BEGIN/END----- headers.\n # Required for production signature validation (via xmlsec / python3-saml).\n SAML_IDP_CERT = os.environ.get(\"SAML_IDP_CERT\", \"\")\n # SP Entity ID — typically the platform's base URL\n SAML_SP_ENTITY_ID = os.environ.get(\"SAML_SP_ENTITY_ID\", \"\")\n # SP ACS URL — leave empty to auto-derive from url_for('account.saml_acs')\n SAML_SP_ACS_URL = os.environ.get(\"SAML_SP_ACS_URL\", \"\")\n\n # Internationalization (S2-01) — date/number/currency formatting\n # Full string translation (gettext) is Phase 2.\n BABEL_DEFAULT_LOCALE = os.environ.get(\"BABEL_DEFAULT_LOCALE\", \"en\")\n BABEL_DEFAULT_TIMEZONE = os.environ.get(\"BABEL_DEFAULT_TIMEZONE\", \"UTC\")\n\n # Read Replica Configuration (ENH-012)\n # Set DATABASE_READ_REPLICA_URL to a replica DB connection string to offload\n # dashboard and portfolio GET queries from the primary. When not configured,\n # all queries use the default SQLALCHEMY_DATABASE_URI (no-op).\n DATABASE_READ_REPLICA_URL = os.environ.get(\"DATABASE_READ_REPLICA_URL\", \"\")\n USE_READ_REPLICA_FOR_DASHBOARD = (\n os.environ.get(\"USE_READ_REPLICA_FOR_DASHBOARD\", \"false\").lower() == \"true\"\n )\n\n # AI-originated CRUD approval gate (A95-008)\n # When true, AI CRUD endpoints return 202 pending_approval instead of writing directly.\n REQUIRE_AI_APPROVAL = os.environ.get(\"REQUIRE_AI_APPROVAL\", \"false\").lower() == \"true\"\n\n # North Star Navigation (NORTH-STAR-001)\n # Enterprise-grade navigation for Fortune 500 TOGAF/ArchiMate practitioners.\n # When enabled: renamed navigation items, persona-based filtering, hidden admin items.\n # Rollback: set to false and restart service (zero risk, instant revert).\n # ENABLED 2026-04-02: Quick wins from NORTH_STAR_ASSESSMENT.md Part 10\n ENABLE_NORTH_STAR_NAV = _env_bool(\"ENABLE_NORTH_STAR_NAV\", True)\n\n # North Star Navigation Phase 2 (NORTH-STAR-002)\n # Complete ArchiMate 3.2 layer-based navigation with all 55 element types.\n # When enabled: 6-section IA, layer-first architecture, Portfolio/Architecture separation.\n # Depends on: ENABLE_NORTH_STAR_NAV=True (Phase 1 must be active)\n # Rollback: set to false and restart service.\n ENABLE_NORTH_STAR_PHASE2 = _env_bool(\"ENABLE_NORTH_STAR_PHASE2\", False)\n\n # Page-aware AI guide is fail-closed by default and only activates when\n # this flag is explicitly enabled and an LLM provider is configured.\n AI_PAGE_GUIDE_ENABLED = _env_bool(\"AI_PAGE_GUIDE_ENABLED\", False)\n\n # File Upload Settings\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max file size\n ALLOWED_EXTENSIONS = {\n \"pdf\",\n \"docx\",\n \"doc\",\n \"xlsx\",\n \"xls\",\n \"png\",\n \"jpg\",\n \"jpeg\",\n \"vsdx\",\n \"txt\",\n \"csv\",\n \"md\",\n }\n UPLOAD_FOLDER = os.path.join(basedir, \"uploads\", \"documents\")\n\n @staticmethod\n def init_app(app):\n \"\"\"Base class hook — subclasses override to customize app initialization.\"\"\"\n return\n\n\nclass DevelopmentConfig(Config):\n DEBUG = True\n ASSETS_DEBUG = True\n\n # N+1 query detection — set SQLALCHEMY_ECHO=true in .env to enable query logging\n SQLALCHEMY_ECHO = os.environ.get(\"SQLALCHEMY_ECHO\", \"false\").lower() == \"true\"\n JIRA_AUTO_PUSH = os.environ.get(\"JIRA_AUTO_PUSH\", \"false\").lower() == \"true\"\n\n # PostgreSQL configuration (REQUIRED - validated at runtime)\n # Default to a local Postgres instance for development when DATABASE_URL is not set.\n # Standard port 5432; set DATABASE_URL in .env to override (e.g. Windows installs\n # that run Postgres on 5439).\n SQLALCHEMY_DATABASE_URI = os.environ.get(\n \"DATABASE_URL\",\n \"postgresql://postgres:postgres@127.0.0.1:5432/archie\", # secrets-safety-ok\n )\n\n # SERVER_NAME must NOT be hardcoded — Flask-WTF CSRF validates the Referer\n # header against SERVER_NAME, so a hardcoded value causes \"CSRF token is missing\"\n # when the browser omits the Referer header (privacy settings, extensions, etc.).\n # Only set when explicitly provided via environment variable.\n SERVER_NAME = os.environ.get(\"SERVER_NAME\") or None\n PREFERRED_URL_SCHEME = os.environ.get(\"PREFERRED_URL_SCHEME\", \"http\")\n APPLICATION_ROOT = os.environ.get(\"APPLICATION_ROOT\", \"/\")\n\n # PostgreSQL connection options (optimized for local development)\n SQLALCHEMY_ENGINE_OPTIONS = {\n \"pool_pre_ping\": True,\n \"pool_recycle\": 300,\n \"pool_size\": 5,\n \"max_overflow\": 10,\n \"pool_timeout\": 30,\n \"connect_args\": {\n \"client_encoding\": \"utf8\",\n \"options\": \"-c client_encoding=utf8\",\n },\n \"pool_reset_on_return\": \"commit\",\n }\n\n @classmethod\n def init_app(cls, app):\n # Ensure a sensible default exists for development (Postgres is the standard)\n if not app.config.get(\"SQLALCHEMY_DATABASE_URI\"):\n app.logger.warning(\n \"No DATABASE_URL provided; falling back to local Postgres default.\"\n )\n\n\nclass TestingConfig(Config):\n TESTING = True\n WTF_CSRF_ENABLED = False\n\n # PostgreSQL REQUIRED for tests (matches production behavior)\n # SQLite is NOT supported - tests must use PostgreSQL for consistency\n SQLALCHEMY_DATABASE_URI = os.environ.get(\n \"TEST_DATABASE_URL\",\n \"postgresql://postgres:postgres@127.0.0.1:5432/archie_test\", # secrets-safety-ok\n )\n\n # PostgreSQL connection options for testing\n SQLALCHEMY_ENGINE_OPTIONS = {\n \"pool_pre_ping\": True,\n \"pool_size\": 5,\n \"max_overflow\": 10,\n }\n\n @classmethod\n def init_app(cls, app):\n # Validate PostgreSQL is being used (not SQLite)\n db_uri = app.config.get(\"SQLALCHEMY_DATABASE_URI\", \"\")\n if \"sqlite\" in db_uri.lower():\n raise ValueError(\n \"TEST_DATABASE_URL must be PostgreSQL, not SQLite. \"\n \"Set TEST_DATABASE_URL to a PostgreSQL connection string. \"\n \"Example: postgresql://postgres:postgres@localhost:5432/flask_test\" # secrets-safety-ok\n )\n\n if not db_uri:\n app.logger.warning(\n \"No TEST_DATABASE_URL provided; using default local PostgreSQL test database.\"\n )\n\n print(\"THIS APP IS IN TESTING MODE. YOU SHOULD NOT SEE THIS IN PRODUCTION.\")\n\n\nclass ProductionConfig(Config):\n DEBUG = False\n USE_RELOADER = False\n\n # Session cookie security\n _cookie_secure_default = os.environ.get(\"PREFERRED_URL_SCHEME\", \"http\").lower() == \"https\"\n SESSION_COOKIE_SECURE = _env_bool(\"SESSION_COOKIE_SECURE\", _cookie_secure_default)\n SESSION_COOKIE_HTTPONLY = True\n SESSION_COOKIE_SAMESITE = \"Lax\"\n REMEMBER_COOKIE_SECURE = _env_bool(\"REMEMBER_COOKIE_SECURE\", _cookie_secure_default)\n REMEMBER_COOKIE_HTTPONLY = True\n\n # PostgreSQL for production (REQUIRED - validated at runtime)\n SQLALCHEMY_DATABASE_URI = os.environ.get(\"DATABASE_URL\")\n SSL_DISABLE = os.environ.get(\"SSL_DISABLE\", \"False\") == \"True\"\n JIRA_AUTO_PUSH = os.environ.get(\"JIRA_AUTO_PUSH\", \"false\").lower() == \"true\"\n\n # PostgreSQL connection options (optimized for production)\n SQLALCHEMY_ENGINE_OPTIONS = {\n \"pool_pre_ping\": True,\n \"pool_recycle\": 300,\n \"pool_size\": 20,\n \"max_overflow\": 30,\n \"pool_timeout\": 30,\n \"connect_args\": {\n \"client_encoding\": \"utf8\",\n \"options\": \"-c client_encoding=utf8\",\n },\n \"pool_reset_on_return\": \"commit\",\n }\n\n @classmethod\n def init_app(cls, app):\n # Validate DATABASE_URL is set\n if not app.config.get(\"SQLALCHEMY_DATABASE_URI\"):\n raise ValueError(\n \"DATABASE_URL environment variable is required. PostgreSQL must be configured for production.\"\n )\n\n Config.init_app(app)\n assert os.environ.get(\"SECRET_KEY\"), \"SECRET_KEY IS NOT SET!\"\n\n # Trust the nginx reverse proxy's forwarded headers (X-Forwarded-Proto /\n # -For) so request.scheme/host reflect the real client-facing protocol.\n # Without this, behind TLS the app still builds http:// external URLs\n # (breaking OAuth redirect URIs, email links) and treats requests as\n # insecure. nginx is the single trusted hop. Idempotent so a subclass\n # (Heroku/Unix) re-running init_app cannot double-wrap.\n if not getattr(app, \"_proxyfix_applied\", False):\n from werkzeug.middleware.proxy_fix import ProxyFix\n\n app.wsgi_app = ProxyFix(\n app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1\n )\n app._proxyfix_applied = True\n\n if HAS_RAYGUN and flask_raygun and app.config.get(\"RAYGUN_APIKEY\"):\n flask_raygun.Provider(app, app.config[\"RAYGUN_APIKEY\"]).attach()\n\n\nclass HerokuConfig(ProductionConfig):\n @classmethod\n def init_app(cls, app):\n # ProductionConfig.init_app applies ProxyFix (idempotently); no need to\n # re-wrap here.\n ProductionConfig.init_app(app)\n\n\nclass UnixConfig(ProductionConfig):\n @classmethod\n def init_app(cls, app):\n ProductionConfig.init_app(app)\n\n # Log to syslog\n import logging\n from logging.handlers import SysLogHandler\n\n syslog_handler = SysLogHandler()\n syslog_handler.setLevel(logging.WARNING)\n app.logger.addHandler(syslog_handler)\n\n\n# Currency Configuration System\nclass CurrencyConfig:\n \"\"\"Centralized currency configuration for the application\"\"\"\n\n DEFAULT_CURRENCY = os.environ.get(\"DEFAULT_CURRENCY\", \"GBP\")\n\n SUPPORTED_CURRENCIES = {\n \"GBP\": {\n \"symbol\": \"£\",\n \"code\": \"GBP\",\n \"position\": \"prefix\", # prefix or suffix\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"British Pound Sterling\",\n },\n \"USD\": {\n \"symbol\": \"$\",\n \"code\": \"USD\",\n \"position\": \"prefix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"United States Dollar\",\n },\n \"EUR\": {\n \"symbol\": \"€\",\n \"code\": \"EUR\",\n \"position\": \"suffix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \".\",\n \"name\": \"Euro\",\n },\n \"JPY\": {\n \"symbol\": \"¥\",\n \"code\": \"JPY\",\n \"position\": \"prefix\",\n \"decimal_places\": 0,\n \"thousands_separator\": \",\",\n \"name\": \"Japanese Yen\",\n },\n \"CAD\": {\n \"symbol\": \"C$\",\n \"code\": \"CAD\",\n \"position\": \"prefix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"Canadian Dollar\",\n },\n \"AUD\": {\n \"symbol\": \"A$\",\n \"code\": \"AUD\",\n \"position\": \"prefix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"Australian Dollar\",\n },\n }\n\n @classmethod\n def get_currency_config(cls, currency_code=None):\n \"\"\"Get currency configuration for specified currency code\"\"\"\n if not currency_code:\n currency_code = cls.DEFAULT_CURRENCY\n\n config = cls.SUPPORTED_CURRENCIES.get(currency_code)\n if not config:\n # Fallback to default currency instead of crashing\n config = cls.SUPPORTED_CURRENCIES.get(cls.DEFAULT_CURRENCY)\n\n return config\n\n @classmethod\n def is_supported(cls, currency_code):\n \"\"\"Check if currency is supported\"\"\"\n return currency_code in cls.SUPPORTED_CURRENCIES\n\n @classmethod\n def get_all_supported_codes(cls):\n \"\"\"Get list of all supported currency codes\"\"\"\n return list(cls.SUPPORTED_CURRENCIES.keys())\n\n\nconfig = {\n \"development\": DevelopmentConfig,\n \"testing\": TestingConfig,\n \"production\": ProductionConfig,\n \"default\": DevelopmentConfig,\n \"heroku\": HerokuConfig,\n \"unix\": UnixConfig,\n}\n"}, "aff7e1b147b4aeaa276d3ef3dff7c220af81791d": {"t": 1785753435.9463027, "n": 20061, "h": "bb62076d8144dda5", "c": "import os\nfrom datetime import timedelta\n\n# Optional raygun import\ntry:\n from raygun4py.middleware import flask as flask_raygun\n\n HAS_RAYGUN = True\nexcept ImportError:\n HAS_RAYGUN = False\n flask_raygun = None\n\n# Python 3 only (Python 2 is no longer supported)\nimport urllib.parse\n\nbasedir = os.path.abspath(os.path.dirname(__file__))\n\n# Canonical module configuration. The v2 (\"guardrail\") modules are the active code\n# path (see CLAUDE.md), and the navigation/templates reference their endpoints, so\n# they MUST be registered by default. Without these flags a fresh install registers\n# only legacy/partial blueprints and every authenticated page 500s on a sidebar\n# url_for to an unregistered endpoint (e.g. solution_design.list_solutions). Using\n# setdefault means an explicit env value (true/false) is still respected.\nfor _canonical_flag in (\n \"USE_SOLUTIONS_STRATEGIC_GUARDRAILS\",\n \"USE_NEW_SOLUTIONS_STRATEGIC\",\n \"USE_CAPABILITIES_GUARDRAILS\",\n \"USE_NEW_CAPABILITIES\",\n \"USE_DASHBOARD_GUARDRAILS\",\n \"USE_NEW_DASHBOARD\",\n \"USE_APPLICATIONS_GUARDRAILS\",\n \"USE_NEW_APPLICATIONS\",\n \"USE_ARCHITECTURE_GUARDRAILS\",\n \"USE_NEW_ARCHITECTURE\",\n \"USE_AI_CHAT_GUARDRAILS\",\n \"USE_NEW_AI_CHAT\",\n \"USE_VENDORS_GUARDRAILS\",\n \"USE_NEW_VENDORS\",\n):\n os.environ.setdefault(_canonical_flag, \"true\")\n\n\ndef _env_bool(name: str, default: bool) -> bool:\n value = os.environ.get(name)\n if value is None:\n return default\n return value.strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\n\n# Load environment variables from .env file\nfrom dotenv import load_dotenv\n\n# Load from .env file using explicit path (ensures it works regardless of working directory)\nenv_path = os.path.join(basedir, \".env\")\nif os.path.exists(env_path):\n load_dotenv(env_path)\n print(f\"Loaded environment from: {env_path}\")\nelse:\n # Fallback to default behavior\n load_dotenv()\n print(\"Loaded environment from default location\")\n\n# Verify critical API keys are loaded\n_api_keys_loaded = []\nfor key in [\n \"OPENAI_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"GEMINI_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"HUGGINGFACE_API_KEY\",\n]:\n if os.getenv(key):\n _api_keys_loaded.append(key)\nif _api_keys_loaded:\n print(f\"API keys loaded: {', '.join(_api_keys_loaded)}\")\nelse:\n print(\"WARNING: No LLM API keys found in environment!\")\n\n\nclass Config:\n APP_NAME = os.environ.get(\"APP_NAME\", \"A.R.C.H.I.E.\")\n SECRET_KEY = os.environ.get(\"SECRET_KEY\")\n if not SECRET_KEY:\n import secrets\n\n SECRET_KEY = secrets.token_hex(32)\n print(\n \"WARNING: SECRET_KEY env var not set. A random key has been generated \"\n \"for this session. Sessions will NOT persist across restarts. \"\n \"Set SECRET_KEY in your .env file for production use.\"\n )\n SQLALCHEMY_COMMIT_ON_TEARDOWN = True\n\n # Session security — 8-hour session lifetime, 30-day remember-me cookie\n PERMANENT_SESSION_LIFETIME = timedelta(hours=8)\n REMEMBER_COOKIE_DURATION = timedelta(days=30)\n SESSION_REFRESH_EACH_REQUEST = True # FAR-012: Ensure session is refreshed on every request\n REMEMBER_COOKIE_REFRESH_EACH_REQUEST = True # FAR-012: Refresh remember-me cookie on activity\n\n # JWT Configuration\n JWT_TOKEN_LOCATION = [\"headers\"]\n JWT_SECRET_KEY = os.environ.get(\"JWT_SECRET_KEY\") or SECRET_KEY\n JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1)\n\n # Email\n MAIL_SERVER = os.environ.get(\"MAIL_SERVER\", \"smtp.sendgrid.net\")\n MAIL_PORT = os.environ.get(\"MAIL_PORT\", 587)\n MAIL_USE_TLS = os.environ.get(\"MAIL_USE_TLS\", True)\n MAIL_USE_SSL = os.environ.get(\"MAIL_USE_SSL\", False)\n MAIL_USERNAME = os.environ.get(\"MAIL_USERNAME\")\n MAIL_PASSWORD = os.environ.get(\"MAIL_PASSWORD\")\n MAIL_DEFAULT_SENDER = os.environ.get(\"MAIL_DEFAULT_SENDER\")\n\n # Analytics\n GOOGLE_ANALYTICS_ID = os.environ.get(\"GOOGLE_ANALYTICS_ID\", \"\")\n SEGMENT_API_KEY = os.environ.get(\"SEGMENT_API_KEY\", \"\")\n\n # Admin account\n ADMIN_PASSWORD = os.environ.get(\"ADMIN_PASSWORD\")\n if not ADMIN_PASSWORD:\n import secrets\n\n ADMIN_PASSWORD = secrets.token_urlsafe(16)\n print(\n \"WARNING: ADMIN_PASSWORD env var not set. Generated random password. \"\n \"Set ADMIN_PASSWORD in your .env file for production use.\"\n )\n ADMIN_EMAIL = os.environ.get(\"ADMIN_EMAIL\", \"flask-base-admin@example.com\")\n EMAIL_SUBJECT_PREFIX = \"[{}]\".format(APP_NAME)\n EMAIL_SENDER = \"{app_name} Admin <{email}>\".format(\n app_name=APP_NAME, email=MAIL_USERNAME\n )\n\n # ARCHIE Deploy: Credential encryption + Coolify PaaS + n8n connector sync\n CREDENTIAL_ENCRYPTION_KEY = os.environ.get(\"CREDENTIAL_ENCRYPTION_KEY\", \"\")\n COOLIFY_API_URL = os.environ.get(\"COOLIFY_API_URL\", \"http://localhost:8000\")\n COOLIFY_API_TOKEN = os.environ.get(\"COOLIFY_API_TOKEN\", \"\")\n COOLIFY_DOMAIN_SUFFIX = os.environ.get(\"COOLIFY_DOMAIN_SUFFIX\", \"archie.example.com\")\n N8N_API_URL = os.environ.get(\"N8N_API_URL\", \"http://localhost:5678\")\n N8N_API_TOKEN = os.environ.get(\"N8N_API_TOKEN\", \"\")\n\n REDIS_URL = os.getenv(\"REDIS_URL\") or os.getenv(\n \"REDISTOGO_URL\", \"redis://localhost:6379/0\"\n )\n\n RAYGUN_APIKEY = os.environ.get(\"RAYGUN_APIKEY\")\n\n # Stripe Billing (COM-001) — platform works without these; set in production .env\n STRIPE_SECRET_KEY = os.environ.get(\"STRIPE_SECRET_KEY\", \"\")\n STRIPE_WEBHOOK_SECRET = os.environ.get(\"STRIPE_WEBHOOK_SECRET\", \"\")\n STRIPE_PRICE_PRO = os.environ.get(\"STRIPE_PRICE_PRO\", \"\")\n STRIPE_PRICE_ENTERPRISE = os.environ.get(\"STRIPE_PRICE_ENTERPRISE\", \"\")\n\n # Parse the REDIS_URL to set RQ config variables\n urllib.parse.uses_netloc.append(\"redis\")\n url = urllib.parse.urlparse(REDIS_URL)\n\n RQ_DEFAULT_HOST = url.hostname\n RQ_DEFAULT_PORT = url.port\n RQ_DEFAULT_PASSWORD = url.password\n RQ_DEFAULT_DB = 0\n\n # Celery configuration (async task queue)\n # Feature-flagged: set CELERY_ENABLED=true to enable async batch import\n CELERY_ENABLED = os.environ.get(\"CELERY_ENABLED\", \"false\").lower() == \"true\"\n CELERY_BROKER_URL = os.environ.get(\"CELERY_BROKER_URL\", REDIS_URL)\n CELERY_RESULT_BACKEND = os.environ.get(\"CELERY_RESULT_BACKEND\", REDIS_URL)\n CELERY_TASK_SERIALIZER = \"json\"\n CELERY_ACCEPT_CONTENT = [\"json\"]\n CELERY_RESULT_SERIALIZER = \"json\"\n CELERY_TIMEZONE = \"UTC\"\n CELERY_ENABLE_UTC = True\n\n # SSO / Enterprise Identity (S0-01)\n # Supports Azure AD, Okta (OIDC) and any SAML 2.0 IdP (PLT-030).\n # Feature-flagged: SSO routes only active when FeatureFlag(key='sso_authentication') is enabled.\n SSO_PROVIDERS = {\n \"azure\": {\n \"client_id\": os.environ.get(\"AZURE_AD_CLIENT_ID\", \"\"),\n \"client_secret\": os.environ.get(\"AZURE_AD_CLIENT_SECRET\", \"\"),\n \"server_metadata_url\": os.environ.get(\n \"AZURE_AD_METADATA_URL\",\n \"https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration\".format(\n tenant=os.environ.get(\"AZURE_AD_TENANT_ID\", \"common\")\n ),\n ),\n \"client_kwargs\": {\"scope\": \"openid email profile\"},\n },\n \"okta\": {\n \"client_id\": os.environ.get(\"OKTA_CLIENT_ID\", \"\"),\n \"client_secret\": os.environ.get(\"OKTA_CLIENT_SECRET\", \"\"),\n \"server_metadata_url\": os.environ.get(\"OKTA_METADATA_URL\", \"\"),\n \"client_kwargs\": {\"scope\": \"openid email profile\"},\n },\n }\n\n # SAML 2.0 IdP configuration (PLT-030)\n # Set SAML_IDP_SSO_URL + SAML_SP_ENTITY_ID to enable SAML alongside OIDC.\n # Supported IdPs: ADFS, PingFederate, Shibboleth, Okta (SAML), Azure AD (SAML).\n # Routes registered at /account/saml/login, /account/saml/acs, /account/saml/metadata.\n SAML_IDP_SSO_URL = os.environ.get(\"SAML_IDP_SSO_URL\", \"\")\n # IdP Entity ID / Issuer URI (validated against Assertion Issuer element)\n SAML_IDP_ENTITY_ID = os.environ.get(\"SAML_IDP_ENTITY_ID\", \"\")\n # IdP X.509 signing certificate — PEM body without -----BEGIN/END----- headers.\n # Required for production signature validation (via xmlsec / python3-saml).\n SAML_IDP_CERT = os.environ.get(\"SAML_IDP_CERT\", \"\")\n # SP Entity ID — typically the platform's base URL\n SAML_SP_ENTITY_ID = os.environ.get(\"SAML_SP_ENTITY_ID\", \"\")\n # SP ACS URL — leave empty to auto-derive from url_for('account.saml_acs')\n SAML_SP_ACS_URL = os.environ.get(\"SAML_SP_ACS_URL\", \"\")\n\n # Internationalization (S2-01) — date/number/currency formatting\n # Full string translation (gettext) is Phase 2.\n BABEL_DEFAULT_LOCALE = os.environ.get(\"BABEL_DEFAULT_LOCALE\", \"en\")\n BABEL_DEFAULT_TIMEZONE = os.environ.get(\"BABEL_DEFAULT_TIMEZONE\", \"UTC\")\n\n # Read Replica Configuration (ENH-012)\n # Set DATABASE_READ_REPLICA_URL to a replica DB connection string to offload\n # dashboard and portfolio GET queries from the primary. When not configured,\n # all queries use the default SQLALCHEMY_DATABASE_URI (no-op).\n DATABASE_READ_REPLICA_URL = os.environ.get(\"DATABASE_READ_REPLICA_URL\", \"\")\n USE_READ_REPLICA_FOR_DASHBOARD = (\n os.environ.get(\"USE_READ_REPLICA_FOR_DASHBOARD\", \"false\").lower() == \"true\"\n )\n\n # AI-originated CRUD approval gate (A95-008)\n # When true, AI CRUD endpoints return 202 pending_approval instead of writing directly.\n REQUIRE_AI_APPROVAL = os.environ.get(\"REQUIRE_AI_APPROVAL\", \"false\").lower() == \"true\"\n\n # North Star Navigation (NORTH-STAR-001)\n # Enterprise-grade navigation for Fortune 500 TOGAF/ArchiMate practitioners.\n # When enabled: renamed navigation items, persona-based filtering, hidden admin items.\n # Rollback: set to false and restart service (zero risk, instant revert).\n # ENABLED 2026-04-02: Quick wins from NORTH_STAR_ASSESSMENT.md Part 10\n ENABLE_NORTH_STAR_NAV = _env_bool(\"ENABLE_NORTH_STAR_NAV\", True)\n\n # North Star Navigation Phase 2 (NORTH-STAR-002)\n # Complete ArchiMate 3.2 layer-based navigation with all 55 element types.\n # When enabled: 6-section IA, layer-first architecture, Portfolio/Architecture separation.\n # Depends on: ENABLE_NORTH_STAR_NAV=True (Phase 1 must be active)\n # Rollback: set to false and restart service.\n ENABLE_NORTH_STAR_PHASE2 = _env_bool(\"ENABLE_NORTH_STAR_PHASE2\", False)\n\n # Page-aware AI guide is fail-closed by default and only activates when\n # this flag is explicitly enabled and an LLM provider is configured.\n AI_PAGE_GUIDE_ENABLED = _env_bool(\"AI_PAGE_GUIDE_ENABLED\", False)\n\n # File Upload Settings\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max file size\n ALLOWED_EXTENSIONS = {\n \"pdf\",\n \"docx\",\n \"doc\",\n \"xlsx\",\n \"xls\",\n \"png\",\n \"jpg\",\n \"jpeg\",\n \"vsdx\",\n \"txt\",\n \"csv\",\n \"md\",\n }\n UPLOAD_FOLDER = os.path.join(basedir, \"uploads\", \"documents\")\n\n @staticmethod\n def init_app(app):\n \"\"\"Base class hook — subclasses override to customize app initialization.\"\"\"\n return\n\n\nclass DevelopmentConfig(Config):\n DEBUG = True\n ASSETS_DEBUG = True\n\n # N+1 query detection — set SQLALCHEMY_ECHO=true in .env to enable query logging\n SQLALCHEMY_ECHO = os.environ.get(\"SQLALCHEMY_ECHO\", \"false\").lower() == \"true\"\n JIRA_AUTO_PUSH = os.environ.get(\"JIRA_AUTO_PUSH\", \"false\").lower() == \"true\"\n\n # PostgreSQL configuration (REQUIRED - validated at runtime)\n # Default to a local Postgres instance for development when DATABASE_URL is not set.\n # Standard port 5432; set DATABASE_URL in .env to override (e.g. Windows installs\n # that run Postgres on 5439).\n SQLALCHEMY_DATABASE_URI = os.environ.get(\n \"DATABASE_URL\",\n \"postgresql://postgres:postgres@127.0.0.1:5432/archie\", # secrets-safety-ok\n )\n\n # SERVER_NAME must NOT be hardcoded — Flask-WTF CSRF validates the Referer\n # header against SERVER_NAME, so a hardcoded value causes \"CSRF token is missing\"\n # when the browser omits the Referer header (privacy settings, extensions, etc.).\n # Only set when explicitly provided via environment variable.\n SERVER_NAME = os.environ.get(\"SERVER_NAME\") or None\n PREFERRED_URL_SCHEME = os.environ.get(\"PREFERRED_URL_SCHEME\", \"http\")\n APPLICATION_ROOT = os.environ.get(\"APPLICATION_ROOT\", \"/\")\n\n # PostgreSQL connection options (optimized for local development)\n SQLALCHEMY_ENGINE_OPTIONS = {\n \"pool_pre_ping\": True,\n \"pool_recycle\": 300,\n \"pool_size\": 5,\n \"max_overflow\": 10,\n \"pool_timeout\": 30,\n \"connect_args\": {\n \"client_encoding\": \"utf8\",\n \"options\": \"-c client_encoding=utf8\",\n },\n \"pool_reset_on_return\": \"commit\",\n }\n\n @classmethod\n def init_app(cls, app):\n # Ensure a sensible default exists for development (Postgres is the standard)\n if not app.config.get(\"SQLALCHEMY_DATABASE_URI\"):\n app.logger.warning(\n \"No DATABASE_URL provided; falling back to local Postgres default.\"\n )\n\n\nclass TestingConfig(Config):\n TESTING = True\n WTF_CSRF_ENABLED = False\n\n # PostgreSQL REQUIRED for tests (matches production behavior)\n # SQLite is NOT supported - tests must use PostgreSQL for consistency\n SQLALCHEMY_DATABASE_URI = os.environ.get(\n \"TEST_DATABASE_URL\",\n \"postgresql://postgres:postgres@127.0.0.1:5432/archie_test\", # secrets-safety-ok\n )\n\n # PostgreSQL connection options for testing\n SQLALCHEMY_ENGINE_OPTIONS = {\n \"pool_pre_ping\": True,\n \"pool_size\": 5,\n \"max_overflow\": 10,\n }\n\n @classmethod\n def init_app(cls, app):\n # Validate PostgreSQL is being used (not SQLite)\n db_uri = app.config.get(\"SQLALCHEMY_DATABASE_URI\", \"\")\n if \"sqlite\" in db_uri.lower():\n raise ValueError(\n \"TEST_DATABASE_URL must be PostgreSQL, not SQLite. \"\n \"Set TEST_DATABASE_URL to a PostgreSQL connection string. \"\n \"Example: postgresql://postgres:postgres@localhost:5432/flask_test\" # secrets-safety-ok\n )\n\n if not db_uri:\n app.logger.warning(\n \"No TEST_DATABASE_URL provided; using default local PostgreSQL test database.\"\n )\n\n print(\"THIS APP IS IN TESTING MODE. YOU SHOULD NOT SEE THIS IN PRODUCTION.\")\n\n\nclass ProductionConfig(Config):\n DEBUG = False\n USE_RELOADER = False\n\n # Session cookie security\n _cookie_secure_default = os.environ.get(\"PREFERRED_URL_SCHEME\", \"http\").lower() == \"https\"\n SESSION_COOKIE_SECURE = _env_bool(\"SESSION_COOKIE_SECURE\", _cookie_secure_default)\n SESSION_COOKIE_HTTPONLY = True\n SESSION_COOKIE_SAMESITE = \"Lax\"\n REMEMBER_COOKIE_SECURE = _env_bool(\"REMEMBER_COOKIE_SECURE\", _cookie_secure_default)\n REMEMBER_COOKIE_HTTPONLY = True\n\n # PostgreSQL for production (REQUIRED - validated at runtime)\n SQLALCHEMY_DATABASE_URI = os.environ.get(\"DATABASE_URL\")\n SSL_DISABLE = os.environ.get(\"SSL_DISABLE\", \"False\") == \"True\"\n JIRA_AUTO_PUSH = os.environ.get(\"JIRA_AUTO_PUSH\", \"false\").lower() == \"true\"\n\n # PostgreSQL connection options (optimized for production)\n SQLALCHEMY_ENGINE_OPTIONS = {\n \"pool_pre_ping\": True,\n \"pool_recycle\": 300,\n \"pool_size\": 20,\n \"max_overflow\": 30,\n \"pool_timeout\": 30,\n \"connect_args\": {\n \"client_encoding\": \"utf8\",\n \"options\": \"-c client_encoding=utf8\",\n },\n \"pool_reset_on_return\": \"commit\",\n }\n\n @classmethod\n def init_app(cls, app):\n # Validate DATABASE_URL is set\n if not app.config.get(\"SQLALCHEMY_DATABASE_URI\"):\n raise ValueError(\n \"DATABASE_URL environment variable is required. PostgreSQL must be configured for production.\"\n )\n\n Config.init_app(app)\n assert os.environ.get(\"SECRET_KEY\"), \"SECRET_KEY IS NOT SET!\"\n\n # Trust the nginx reverse proxy's forwarded headers (X-Forwarded-Proto /\n # -For) so request.scheme/host reflect the real client-facing protocol.\n # Without this, behind TLS the app still builds http:// external URLs\n # (breaking OAuth redirect URIs, email links) and treats requests as\n # insecure. nginx is the single trusted hop. Idempotent so a subclass\n # (Heroku/Unix) re-running init_app cannot double-wrap.\n if not getattr(app, \"_proxyfix_applied\", False):\n from werkzeug.middleware.proxy_fix import ProxyFix\n\n app.wsgi_app = ProxyFix(\n app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1\n )\n app._proxyfix_applied = True\n\n if HAS_RAYGUN and flask_raygun and app.config.get(\"RAYGUN_APIKEY\"):\n flask_raygun.Provider(app, app.config[\"RAYGUN_APIKEY\"]).attach()\n\n\nclass HerokuConfig(ProductionConfig):\n @classmethod\n def init_app(cls, app):\n # ProductionConfig.init_app applies ProxyFix (idempotently); no need to\n # re-wrap here.\n ProductionConfig.init_app(app)\n\n\nclass UnixConfig(ProductionConfig):\n @classmethod\n def init_app(cls, app):\n ProductionConfig.init_app(app)\n\n # Log to syslog\n import logging\n from logging.handlers import SysLogHandler\n\n syslog_handler = SysLogHandler()\n syslog_handler.setLevel(logging.WARNING)\n app.logger.addHandler(syslog_handler)\n\n\n# Currency Configuration System\nclass CurrencyConfig:\n \"\"\"Centralized currency configuration for the application\"\"\"\n\n DEFAULT_CURRENCY = os.environ.get(\"DEFAULT_CURRENCY\", \"GBP\")\n\n SUPPORTED_CURRENCIES = {\n \"GBP\": {\n \"symbol\": \"£\",\n \"code\": \"GBP\",\n \"position\": \"prefix\", # prefix or suffix\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"British Pound Sterling\",\n },\n \"USD\": {\n \"symbol\": \"$\",\n \"code\": \"USD\",\n \"position\": \"prefix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"United States Dollar\",\n },\n \"EUR\": {\n \"symbol\": \"€\",\n \"code\": \"EUR\",\n \"position\": \"suffix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \".\",\n \"name\": \"Euro\",\n },\n \"JPY\": {\n \"symbol\": \"¥\",\n \"code\": \"JPY\",\n \"position\": \"prefix\",\n \"decimal_places\": 0,\n \"thousands_separator\": \",\",\n \"name\": \"Japanese Yen\",\n },\n \"CAD\": {\n \"symbol\": \"C$\",\n \"code\": \"CAD\",\n \"position\": \"prefix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"Canadian Dollar\",\n },\n \"AUD\": {\n \"symbol\": \"A$\",\n \"code\": \"AUD\",\n \"position\": \"prefix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"Australian Dollar\",\n },\n }\n\n @classmethod\n def get_currency_config(cls, currency_code=None):\n \"\"\"Get currency configuration for specified currency code\"\"\"\n if not currency_code:\n currency_code = cls.DEFAULT_CURRENCY\n\n config = cls.SUPPORTED_CURRENCIES.get(currency_code)\n if not config:\n # Fallback to default currency instead of crashing\n config = cls.SUPPORTED_CURRENCIES.get(cls.DEFAULT_CURRENCY)\n\n return config\n\n @classmethod\n def is_supported(cls, currency_code):\n \"\"\"Check if currency is supported\"\"\"\n return currency_code in cls.SUPPORTED_CURRENCIES\n\n @classmethod\n def get_all_supported_codes(cls):\n \"\"\"Get list of all supported currency codes\"\"\"\n return list(cls.SUPPORTED_CURRENCIES.keys())\n\n\nconfig = {\n \"development\": DevelopmentConfig,\n \"testing\": TestingConfig,\n \"production\": ProductionConfig,\n \"default\": DevelopmentConfig,\n \"heroku\": HerokuConfig,\n \"unix\": UnixConfig,\n}\n"}, "33f31e704f1a301da1e85f5d4bbe39993fa44934": {"t": 1785753437.5126364, "n": 19065, "h": "4de1badadec46cea", "c": "import os\nfrom datetime import timedelta\n\n# Optional raygun import\ntry:\n from raygun4py.middleware import flask as flask_raygun\n\n HAS_RAYGUN = True\nexcept ImportError:\n HAS_RAYGUN = False\n flask_raygun = None\n\n# Python 3 only (Python 2 is no longer supported)\nimport urllib.parse\n\nbasedir = os.path.abspath(os.path.dirname(__file__))\n\n\ndef _env_bool(name: str, default: bool) -> bool:\n value = os.environ.get(name)\n if value is None:\n return default\n return value.strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\n\n# Load environment variables from .env file\nfrom dotenv import load_dotenv\n\n# Load from .env file using explicit path (ensures it works regardless of working directory)\nenv_path = os.path.join(basedir, \".env\")\nif os.path.exists(env_path):\n load_dotenv(env_path)\n print(f\"Loaded environment from: {env_path}\")\nelse:\n # Fallback to default behavior\n load_dotenv()\n print(\"Loaded environment from default location\")\n\n# Verify critical API keys are loaded\n_api_keys_loaded = []\nfor key in [\n \"OPENAI_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"GEMINI_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"HUGGINGFACE_API_KEY\",\n]:\n if os.getenv(key):\n _api_keys_loaded.append(key)\nif _api_keys_loaded:\n print(f\"API keys loaded: {', '.join(_api_keys_loaded)}\")\nelse:\n print(\"WARNING: No LLM API keys found in environment!\")\n\n\nclass Config:\n APP_NAME = os.environ.get(\"APP_NAME\", \"A.R.C.H.I.E.\")\n SECRET_KEY = os.environ.get(\"SECRET_KEY\")\n if not SECRET_KEY:\n import secrets\n\n SECRET_KEY = secrets.token_hex(32)\n print(\n \"WARNING: SECRET_KEY env var not set. A random key has been generated \"\n \"for this session. Sessions will NOT persist across restarts. \"\n \"Set SECRET_KEY in your .env file for production use.\"\n )\n SQLALCHEMY_COMMIT_ON_TEARDOWN = True\n\n # Session security — 8-hour session lifetime, 30-day remember-me cookie\n PERMANENT_SESSION_LIFETIME = timedelta(hours=8)\n REMEMBER_COOKIE_DURATION = timedelta(days=30)\n SESSION_REFRESH_EACH_REQUEST = True # FAR-012: Ensure session is refreshed on every request\n REMEMBER_COOKIE_REFRESH_EACH_REQUEST = True # FAR-012: Refresh remember-me cookie on activity\n\n # JWT Configuration\n JWT_TOKEN_LOCATION = [\"headers\"]\n JWT_SECRET_KEY = os.environ.get(\"JWT_SECRET_KEY\") or SECRET_KEY\n JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1)\n\n # Email\n MAIL_SERVER = os.environ.get(\"MAIL_SERVER\", \"smtp.sendgrid.net\")\n MAIL_PORT = os.environ.get(\"MAIL_PORT\", 587)\n MAIL_USE_TLS = os.environ.get(\"MAIL_USE_TLS\", True)\n MAIL_USE_SSL = os.environ.get(\"MAIL_USE_SSL\", False)\n MAIL_USERNAME = os.environ.get(\"MAIL_USERNAME\")\n MAIL_PASSWORD = os.environ.get(\"MAIL_PASSWORD\")\n MAIL_DEFAULT_SENDER = os.environ.get(\"MAIL_DEFAULT_SENDER\")\n\n # Analytics\n GOOGLE_ANALYTICS_ID = os.environ.get(\"GOOGLE_ANALYTICS_ID\", \"\")\n SEGMENT_API_KEY = os.environ.get(\"SEGMENT_API_KEY\", \"\")\n\n # Admin account\n ADMIN_PASSWORD = os.environ.get(\"ADMIN_PASSWORD\")\n if not ADMIN_PASSWORD:\n import secrets\n\n ADMIN_PASSWORD = secrets.token_urlsafe(16)\n print(\n \"WARNING: ADMIN_PASSWORD env var not set. Generated random password. \"\n \"Set ADMIN_PASSWORD in your .env file for production use.\"\n )\n ADMIN_EMAIL = os.environ.get(\"ADMIN_EMAIL\", \"flask-base-admin@example.com\")\n EMAIL_SUBJECT_PREFIX = \"[{}]\".format(APP_NAME)\n EMAIL_SENDER = \"{app_name} Admin <{email}>\".format(\n app_name=APP_NAME, email=MAIL_USERNAME\n )\n\n # ARCHIE Deploy: Credential encryption + Coolify PaaS + n8n connector sync\n CREDENTIAL_ENCRYPTION_KEY = os.environ.get(\"CREDENTIAL_ENCRYPTION_KEY\", \"\")\n COOLIFY_API_URL = os.environ.get(\"COOLIFY_API_URL\", \"http://localhost:8000\")\n COOLIFY_API_TOKEN = os.environ.get(\"COOLIFY_API_TOKEN\", \"\")\n COOLIFY_DOMAIN_SUFFIX = os.environ.get(\"COOLIFY_DOMAIN_SUFFIX\", \"archie.example.com\")\n N8N_API_URL = os.environ.get(\"N8N_API_URL\", \"http://localhost:5678\")\n N8N_API_TOKEN = os.environ.get(\"N8N_API_TOKEN\", \"\")\n\n REDIS_URL = os.getenv(\"REDIS_URL\") or os.getenv(\n \"REDISTOGO_URL\", \"redis://localhost:6379/0\"\n )\n\n RAYGUN_APIKEY = os.environ.get(\"RAYGUN_APIKEY\")\n\n # Stripe Billing (COM-001) — platform works without these; set in production .env\n STRIPE_SECRET_KEY = os.environ.get(\"STRIPE_SECRET_KEY\", \"\")\n STRIPE_WEBHOOK_SECRET = os.environ.get(\"STRIPE_WEBHOOK_SECRET\", \"\")\n STRIPE_PRICE_PRO = os.environ.get(\"STRIPE_PRICE_PRO\", \"\")\n STRIPE_PRICE_ENTERPRISE = os.environ.get(\"STRIPE_PRICE_ENTERPRISE\", \"\")\n\n # Parse the REDIS_URL to set RQ config variables\n urllib.parse.uses_netloc.append(\"redis\")\n url = urllib.parse.urlparse(REDIS_URL)\n\n RQ_DEFAULT_HOST = url.hostname\n RQ_DEFAULT_PORT = url.port\n RQ_DEFAULT_PASSWORD = url.password\n RQ_DEFAULT_DB = 0\n\n # Celery configuration (async task queue)\n # Feature-flagged: set CELERY_ENABLED=true to enable async batch import\n CELERY_ENABLED = os.environ.get(\"CELERY_ENABLED\", \"false\").lower() == \"true\"\n CELERY_BROKER_URL = os.environ.get(\"CELERY_BROKER_URL\", REDIS_URL)\n CELERY_RESULT_BACKEND = os.environ.get(\"CELERY_RESULT_BACKEND\", REDIS_URL)\n CELERY_TASK_SERIALIZER = \"json\"\n CELERY_ACCEPT_CONTENT = [\"json\"]\n CELERY_RESULT_SERIALIZER = \"json\"\n CELERY_TIMEZONE = \"UTC\"\n CELERY_ENABLE_UTC = True\n\n # SSO / Enterprise Identity (S0-01)\n # Supports Azure AD, Okta (OIDC) and any SAML 2.0 IdP (PLT-030).\n # Feature-flagged: SSO routes only active when FeatureFlag(key='sso_authentication') is enabled.\n SSO_PROVIDERS = {\n \"azure\": {\n \"client_id\": os.environ.get(\"AZURE_AD_CLIENT_ID\", \"\"),\n \"client_secret\": os.environ.get(\"AZURE_AD_CLIENT_SECRET\", \"\"),\n \"server_metadata_url\": os.environ.get(\n \"AZURE_AD_METADATA_URL\",\n \"https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration\".format(\n tenant=os.environ.get(\"AZURE_AD_TENANT_ID\", \"common\")\n ),\n ),\n \"client_kwargs\": {\"scope\": \"openid email profile\"},\n },\n \"okta\": {\n \"client_id\": os.environ.get(\"OKTA_CLIENT_ID\", \"\"),\n \"client_secret\": os.environ.get(\"OKTA_CLIENT_SECRET\", \"\"),\n \"server_metadata_url\": os.environ.get(\"OKTA_METADATA_URL\", \"\"),\n \"client_kwargs\": {\"scope\": \"openid email profile\"},\n },\n }\n\n # SAML 2.0 IdP configuration (PLT-030)\n # Set SAML_IDP_SSO_URL + SAML_SP_ENTITY_ID to enable SAML alongside OIDC.\n # Supported IdPs: ADFS, PingFederate, Shibboleth, Okta (SAML), Azure AD (SAML).\n # Routes registered at /account/saml/login, /account/saml/acs, /account/saml/metadata.\n SAML_IDP_SSO_URL = os.environ.get(\"SAML_IDP_SSO_URL\", \"\")\n # IdP Entity ID / Issuer URI (validated against Assertion Issuer element)\n SAML_IDP_ENTITY_ID = os.environ.get(\"SAML_IDP_ENTITY_ID\", \"\")\n # IdP X.509 signing certificate — PEM body without -----BEGIN/END----- headers.\n # Required for production signature validation (via xmlsec / python3-saml).\n SAML_IDP_CERT = os.environ.get(\"SAML_IDP_CERT\", \"\")\n # SP Entity ID — typically the platform's base URL\n SAML_SP_ENTITY_ID = os.environ.get(\"SAML_SP_ENTITY_ID\", \"\")\n # SP ACS URL — leave empty to auto-derive from url_for('account.saml_acs')\n SAML_SP_ACS_URL = os.environ.get(\"SAML_SP_ACS_URL\", \"\")\n\n # Internationalization (S2-01) — date/number/currency formatting\n # Full string translation (gettext) is Phase 2.\n BABEL_DEFAULT_LOCALE = os.environ.get(\"BABEL_DEFAULT_LOCALE\", \"en\")\n BABEL_DEFAULT_TIMEZONE = os.environ.get(\"BABEL_DEFAULT_TIMEZONE\", \"UTC\")\n\n # Read Replica Configuration (ENH-012)\n # Set DATABASE_READ_REPLICA_URL to a replica DB connection string to offload\n # dashboard and portfolio GET queries from the primary. When not configured,\n # all queries use the default SQLALCHEMY_DATABASE_URI (no-op).\n DATABASE_READ_REPLICA_URL = os.environ.get(\"DATABASE_READ_REPLICA_URL\", \"\")\n USE_READ_REPLICA_FOR_DASHBOARD = (\n os.environ.get(\"USE_READ_REPLICA_FOR_DASHBOARD\", \"false\").lower() == \"true\"\n )\n\n # AI-originated CRUD approval gate (A95-008)\n # When true, AI CRUD endpoints return 202 pending_approval instead of writing directly.\n REQUIRE_AI_APPROVAL = os.environ.get(\"REQUIRE_AI_APPROVAL\", \"false\").lower() == \"true\"\n\n # North Star Navigation (NORTH-STAR-001)\n # Enterprise-grade navigation for Fortune 500 TOGAF/ArchiMate practitioners.\n # When enabled: renamed navigation items, persona-based filtering, hidden admin items.\n # Rollback: set to false and restart service (zero risk, instant revert).\n # ENABLED 2026-04-02: Quick wins from NORTH_STAR_ASSESSMENT.md Part 10\n ENABLE_NORTH_STAR_NAV = _env_bool(\"ENABLE_NORTH_STAR_NAV\", True)\n\n # North Star Navigation Phase 2 (NORTH-STAR-002)\n # Complete ArchiMate 3.2 layer-based navigation with all 55 element types.\n # When enabled: 6-section IA, layer-first architecture, Portfolio/Architecture separation.\n # Depends on: ENABLE_NORTH_STAR_NAV=True (Phase 1 must be active)\n # Rollback: set to false and restart service.\n ENABLE_NORTH_STAR_PHASE2 = _env_bool(\"ENABLE_NORTH_STAR_PHASE2\", False)\n\n # Page-aware AI guide is fail-closed by default and only activates when\n # this flag is explicitly enabled and an LLM provider is configured.\n AI_PAGE_GUIDE_ENABLED = _env_bool(\"AI_PAGE_GUIDE_ENABLED\", False)\n\n # File Upload Settings\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max file size\n ALLOWED_EXTENSIONS = {\n \"pdf\",\n \"docx\",\n \"doc\",\n \"xlsx\",\n \"xls\",\n \"png\",\n \"jpg\",\n \"jpeg\",\n \"vsdx\",\n \"txt\",\n \"csv\",\n \"md\",\n }\n UPLOAD_FOLDER = os.path.join(basedir, \"uploads\", \"documents\")\n\n @staticmethod\n def init_app(app):\n \"\"\"Base class hook — subclasses override to customize app initialization.\"\"\"\n return\n\n\nclass DevelopmentConfig(Config):\n DEBUG = True\n ASSETS_DEBUG = True\n\n # N+1 query detection — set SQLALCHEMY_ECHO=true in .env to enable query logging\n SQLALCHEMY_ECHO = os.environ.get(\"SQLALCHEMY_ECHO\", \"false\").lower() == \"true\"\n JIRA_AUTO_PUSH = os.environ.get(\"JIRA_AUTO_PUSH\", \"false\").lower() == \"true\"\n\n # PostgreSQL configuration (REQUIRED - validated at runtime)\n # Default to a local Postgres instance for development when DATABASE_URL is not set.\n # Standard port 5432; set DATABASE_URL in .env to override (e.g. Windows installs\n # that run Postgres on 5439).\n SQLALCHEMY_DATABASE_URI = os.environ.get(\n \"DATABASE_URL\",\n \"postgresql://postgres:postgres@127.0.0.1:5432/archie\", # secrets-safety-ok\n )\n\n # SERVER_NAME must NOT be hardcoded — Flask-WTF CSRF validates the Referer\n # header against SERVER_NAME, so a hardcoded value causes \"CSRF token is missing\"\n # when the browser omits the Referer header (privacy settings, extensions, etc.).\n # Only set when explicitly provided via environment variable.\n SERVER_NAME = os.environ.get(\"SERVER_NAME\") or None\n PREFERRED_URL_SCHEME = os.environ.get(\"PREFERRED_URL_SCHEME\", \"http\")\n APPLICATION_ROOT = os.environ.get(\"APPLICATION_ROOT\", \"/\")\n\n # PostgreSQL connection options (optimized for local development)\n SQLALCHEMY_ENGINE_OPTIONS = {\n \"pool_pre_ping\": True,\n \"pool_recycle\": 300,\n \"pool_size\": 5,\n \"max_overflow\": 10,\n \"pool_timeout\": 30,\n \"connect_args\": {\n \"client_encoding\": \"utf8\",\n \"options\": \"-c client_encoding=utf8\",\n },\n \"pool_reset_on_return\": \"commit\",\n }\n\n @classmethod\n def init_app(cls, app):\n # Ensure a sensible default exists for development (Postgres is the standard)\n if not app.config.get(\"SQLALCHEMY_DATABASE_URI\"):\n app.logger.warning(\n \"No DATABASE_URL provided; falling back to local Postgres default.\"\n )\n\n\nclass TestingConfig(Config):\n TESTING = True\n WTF_CSRF_ENABLED = False\n\n # PostgreSQL REQUIRED for tests (matches production behavior)\n # SQLite is NOT supported - tests must use PostgreSQL for consistency\n SQLALCHEMY_DATABASE_URI = os.environ.get(\n \"TEST_DATABASE_URL\",\n \"postgresql://postgres:postgres@127.0.0.1:5432/archie_test\", # secrets-safety-ok\n )\n\n # PostgreSQL connection options for testing\n SQLALCHEMY_ENGINE_OPTIONS = {\n \"pool_pre_ping\": True,\n \"pool_size\": 5,\n \"max_overflow\": 10,\n }\n\n @classmethod\n def init_app(cls, app):\n # Validate PostgreSQL is being used (not SQLite)\n db_uri = app.config.get(\"SQLALCHEMY_DATABASE_URI\", \"\")\n if \"sqlite\" in db_uri.lower():\n raise ValueError(\n \"TEST_DATABASE_URL must be PostgreSQL, not SQLite. \"\n \"Set TEST_DATABASE_URL to a PostgreSQL connection string. \"\n \"Example: postgresql://postgres:postgres@localhost:5432/flask_test\" # secrets-safety-ok\n )\n\n if not db_uri:\n app.logger.warning(\n \"No TEST_DATABASE_URL provided; using default local PostgreSQL test database.\"\n )\n\n print(\"THIS APP IS IN TESTING MODE. YOU SHOULD NOT SEE THIS IN PRODUCTION.\")\n\n\nclass ProductionConfig(Config):\n DEBUG = False\n USE_RELOADER = False\n\n # Session cookie security\n _cookie_secure_default = os.environ.get(\"PREFERRED_URL_SCHEME\", \"http\").lower() == \"https\"\n SESSION_COOKIE_SECURE = _env_bool(\"SESSION_COOKIE_SECURE\", _cookie_secure_default)\n SESSION_COOKIE_HTTPONLY = True\n SESSION_COOKIE_SAMESITE = \"Lax\"\n REMEMBER_COOKIE_SECURE = _env_bool(\"REMEMBER_COOKIE_SECURE\", _cookie_secure_default)\n REMEMBER_COOKIE_HTTPONLY = True\n\n # PostgreSQL for production (REQUIRED - validated at runtime)\n SQLALCHEMY_DATABASE_URI = os.environ.get(\"DATABASE_URL\")\n SSL_DISABLE = os.environ.get(\"SSL_DISABLE\", \"False\") == \"True\"\n JIRA_AUTO_PUSH = os.environ.get(\"JIRA_AUTO_PUSH\", \"false\").lower() == \"true\"\n\n # PostgreSQL connection options (optimized for production)\n SQLALCHEMY_ENGINE_OPTIONS = {\n \"pool_pre_ping\": True,\n \"pool_recycle\": 300,\n \"pool_size\": 20,\n \"max_overflow\": 30,\n \"pool_timeout\": 30,\n \"connect_args\": {\n \"client_encoding\": \"utf8\",\n \"options\": \"-c client_encoding=utf8\",\n },\n \"pool_reset_on_return\": \"commit\",\n }\n\n @classmethod\n def init_app(cls, app):\n # Validate DATABASE_URL is set\n if not app.config.get(\"SQLALCHEMY_DATABASE_URI\"):\n raise ValueError(\n \"DATABASE_URL environment variable is required. PostgreSQL must be configured for production.\"\n )\n\n Config.init_app(app)\n assert os.environ.get(\"SECRET_KEY\"), \"SECRET_KEY IS NOT SET!\"\n\n # Trust the nginx reverse proxy's forwarded headers (X-Forwarded-Proto /\n # -For) so request.scheme/host reflect the real client-facing protocol.\n # Without this, behind TLS the app still builds http:// external URLs\n # (breaking OAuth redirect URIs, email links) and treats requests as\n # insecure. nginx is the single trusted hop. Idempotent so a subclass\n # (Heroku/Unix) re-running init_app cannot double-wrap.\n if not getattr(app, \"_proxyfix_applied\", False):\n from werkzeug.middleware.proxy_fix import ProxyFix\n\n app.wsgi_app = ProxyFix(\n app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1\n )\n app._proxyfix_applied = True\n\n if HAS_RAYGUN and flask_raygun and app.config.get(\"RAYGUN_APIKEY\"):\n flask_raygun.Provider(app, app.config[\"RAYGUN_APIKEY\"]).attach()\n\n\nclass HerokuConfig(ProductionConfig):\n @classmethod\n def init_app(cls, app):\n # ProductionConfig.init_app applies ProxyFix (idempotently); no need to\n # re-wrap here.\n ProductionConfig.init_app(app)\n\n\nclass UnixConfig(ProductionConfig):\n @classmethod\n def init_app(cls, app):\n ProductionConfig.init_app(app)\n\n # Log to syslog\n import logging\n from logging.handlers import SysLogHandler\n\n syslog_handler = SysLogHandler()\n syslog_handler.setLevel(logging.WARNING)\n app.logger.addHandler(syslog_handler)\n\n\n# Currency Configuration System\nclass CurrencyConfig:\n \"\"\"Centralized currency configuration for the application\"\"\"\n\n DEFAULT_CURRENCY = os.environ.get(\"DEFAULT_CURRENCY\", \"GBP\")\n\n SUPPORTED_CURRENCIES = {\n \"GBP\": {\n \"symbol\": \"£\",\n \"code\": \"GBP\",\n \"position\": \"prefix\", # prefix or suffix\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"British Pound Sterling\",\n },\n \"USD\": {\n \"symbol\": \"$\",\n \"code\": \"USD\",\n \"position\": \"prefix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"United States Dollar\",\n },\n \"EUR\": {\n \"symbol\": \"€\",\n \"code\": \"EUR\",\n \"position\": \"suffix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \".\",\n \"name\": \"Euro\",\n },\n \"JPY\": {\n \"symbol\": \"¥\",\n \"code\": \"JPY\",\n \"position\": \"prefix\",\n \"decimal_places\": 0,\n \"thousands_separator\": \",\",\n \"name\": \"Japanese Yen\",\n },\n \"CAD\": {\n \"symbol\": \"C$\",\n \"code\": \"CAD\",\n \"position\": \"prefix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"Canadian Dollar\",\n },\n \"AUD\": {\n \"symbol\": \"A$\",\n \"code\": \"AUD\",\n \"position\": \"prefix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"Australian Dollar\",\n },\n }\n\n @classmethod\n def get_currency_config(cls, currency_code=None):\n \"\"\"Get currency configuration for specified currency code\"\"\"\n if not currency_code:\n currency_code = cls.DEFAULT_CURRENCY\n\n config = cls.SUPPORTED_CURRENCIES.get(currency_code)\n if not config:\n # Fallback to default currency instead of crashing\n config = cls.SUPPORTED_CURRENCIES.get(cls.DEFAULT_CURRENCY)\n\n return config\n\n @classmethod\n def is_supported(cls, currency_code):\n \"\"\"Check if currency is supported\"\"\"\n return currency_code in cls.SUPPORTED_CURRENCIES\n\n @classmethod\n def get_all_supported_codes(cls):\n \"\"\"Get list of all supported currency codes\"\"\"\n return list(cls.SUPPORTED_CURRENCIES.keys())\n\n\nconfig = {\n \"development\": DevelopmentConfig,\n \"testing\": TestingConfig,\n \"production\": ProductionConfig,\n \"default\": DevelopmentConfig,\n \"heroku\": HerokuConfig,\n \"unix\": UnixConfig,\n}\n"}, "f35ff1a649ef9a3bf31df9887690e5070a38cd11": {"t": 1785753440.1442654, "n": 19219, "h": "082695e8549c72d6", "c": "import os\nfrom datetime import timedelta\n\n# Optional raygun import\ntry:\n from raygun4py.middleware import flask as flask_raygun\n\n HAS_RAYGUN = True\nexcept ImportError:\n HAS_RAYGUN = False\n flask_raygun = None\n\n# Python 3 only (Python 2 is no longer supported)\nimport urllib.parse\n\nbasedir = os.path.abspath(os.path.dirname(__file__))\n\n\ndef _env_bool(name: str, default: bool) -> bool:\n value = os.environ.get(name)\n if value is None:\n return default\n return value.strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\n\n# Load environment variables from .env file\nfrom dotenv import load_dotenv\n\n# Load from .env file using explicit path (ensures it works regardless of working directory)\nenv_path = os.path.join(basedir, \".env\")\nif os.path.exists(env_path):\n load_dotenv(env_path)\n print(f\"Loaded environment from: {env_path}\")\nelse:\n # Fallback to default behavior\n load_dotenv()\n print(\"Loaded environment from default location\")\n\n# Verify critical API keys are loaded\n_api_keys_loaded = []\nfor key in [\n \"OPENAI_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"GEMINI_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"HUGGINGFACE_API_KEY\",\n]:\n if os.getenv(key):\n _api_keys_loaded.append(key)\nif _api_keys_loaded:\n print(f\"API keys loaded: {', '.join(_api_keys_loaded)}\")\nelse:\n print(\"WARNING: No LLM API keys found in environment!\")\n\n\nclass Config:\n APP_NAME = os.environ.get(\"APP_NAME\", \"A.R.C.H.I.E.\")\n SECRET_KEY = os.environ.get(\"SECRET_KEY\")\n if not SECRET_KEY:\n import secrets\n\n SECRET_KEY = secrets.token_hex(32)\n print(\n \"WARNING: SECRET_KEY env var not set. A random key has been generated \"\n \"for this session. Sessions will NOT persist across restarts. \"\n \"Set SECRET_KEY in your .env file for production use.\"\n )\n SQLALCHEMY_COMMIT_ON_TEARDOWN = True\n\n # Session security — 8-hour session lifetime, 30-day remember-me cookie\n PERMANENT_SESSION_LIFETIME = timedelta(hours=8)\n REMEMBER_COOKIE_DURATION = timedelta(days=30)\n SESSION_REFRESH_EACH_REQUEST = True # FAR-012: Ensure session is refreshed on every request\n REMEMBER_COOKIE_REFRESH_EACH_REQUEST = True # FAR-012: Refresh remember-me cookie on activity\n\n # JWT Configuration\n JWT_TOKEN_LOCATION = [\"headers\"]\n JWT_SECRET_KEY = os.environ.get(\"JWT_SECRET_KEY\") or SECRET_KEY\n JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1)\n\n # Email\n MAIL_SERVER = os.environ.get(\"MAIL_SERVER\", \"smtp.sendgrid.net\")\n MAIL_PORT = os.environ.get(\"MAIL_PORT\", 587)\n MAIL_USE_TLS = os.environ.get(\"MAIL_USE_TLS\", True)\n MAIL_USE_SSL = os.environ.get(\"MAIL_USE_SSL\", False)\n MAIL_USERNAME = os.environ.get(\"MAIL_USERNAME\")\n MAIL_PASSWORD = os.environ.get(\"MAIL_PASSWORD\")\n MAIL_DEFAULT_SENDER = os.environ.get(\"MAIL_DEFAULT_SENDER\")\n\n # Analytics\n GOOGLE_ANALYTICS_ID = os.environ.get(\"GOOGLE_ANALYTICS_ID\", \"\")\n SEGMENT_API_KEY = os.environ.get(\"SEGMENT_API_KEY\", \"\")\n\n # Admin account\n ADMIN_PASSWORD = os.environ.get(\"ADMIN_PASSWORD\")\n if not ADMIN_PASSWORD:\n import secrets\n\n ADMIN_PASSWORD = secrets.token_urlsafe(16)\n print(\n \"WARNING: ADMIN_PASSWORD env var not set. Generated random password. \"\n \"Set ADMIN_PASSWORD in your .env file for production use.\"\n )\n ADMIN_EMAIL = os.environ.get(\"ADMIN_EMAIL\", \"flask-base-admin@example.com\")\n EMAIL_SUBJECT_PREFIX = \"[{}]\".format(APP_NAME)\n EMAIL_SENDER = \"{app_name} Admin <{email}>\".format(\n app_name=APP_NAME, email=MAIL_USERNAME\n )\n\n # ARCHIE Deploy: Credential encryption + Coolify PaaS + n8n connector sync\n CREDENTIAL_ENCRYPTION_KEY = os.environ.get(\"CREDENTIAL_ENCRYPTION_KEY\", \"\")\n COOLIFY_API_URL = os.environ.get(\"COOLIFY_API_URL\", \"http://localhost:8000\")\n COOLIFY_API_TOKEN = os.environ.get(\"COOLIFY_API_TOKEN\", \"\")\n COOLIFY_DOMAIN_SUFFIX = os.environ.get(\"COOLIFY_DOMAIN_SUFFIX\", \"archie.example.com\")\n N8N_API_URL = os.environ.get(\"N8N_API_URL\", \"http://localhost:5678\")\n N8N_API_TOKEN = os.environ.get(\"N8N_API_TOKEN\", \"\")\n\n REDIS_URL = os.getenv(\"REDIS_URL\") or os.getenv(\n \"REDISTOGO_URL\", \"redis://localhost:6379/0\"\n )\n\n RAYGUN_APIKEY = os.environ.get(\"RAYGUN_APIKEY\")\n\n # Stripe Billing (COM-001) — platform works without these; set in production .env\n STRIPE_SECRET_KEY = os.environ.get(\"STRIPE_SECRET_KEY\", \"\")\n STRIPE_WEBHOOK_SECRET = os.environ.get(\"STRIPE_WEBHOOK_SECRET\", \"\")\n STRIPE_PRICE_PRO = os.environ.get(\"STRIPE_PRICE_PRO\", \"\")\n STRIPE_PRICE_ENTERPRISE = os.environ.get(\"STRIPE_PRICE_ENTERPRISE\", \"\")\n\n # Parse the REDIS_URL to set RQ config variables\n urllib.parse.uses_netloc.append(\"redis\")\n url = urllib.parse.urlparse(REDIS_URL)\n\n RQ_DEFAULT_HOST = url.hostname\n RQ_DEFAULT_PORT = url.port\n RQ_DEFAULT_PASSWORD = url.password\n RQ_DEFAULT_DB = 0\n\n # Celery configuration (async task queue)\n # Feature-flagged: set CELERY_ENABLED=true to enable async batch import\n CELERY_ENABLED = os.environ.get(\"CELERY_ENABLED\", \"false\").lower() == \"true\"\n CELERY_BROKER_URL = os.environ.get(\"CELERY_BROKER_URL\", REDIS_URL)\n CELERY_RESULT_BACKEND = os.environ.get(\"CELERY_RESULT_BACKEND\", REDIS_URL)\n CELERY_TASK_SERIALIZER = \"json\"\n CELERY_ACCEPT_CONTENT = [\"json\"]\n CELERY_RESULT_SERIALIZER = \"json\"\n CELERY_TIMEZONE = \"UTC\"\n CELERY_ENABLE_UTC = True\n\n # SSO / Enterprise Identity (S0-01)\n # Supports Azure AD, Okta (OIDC) and any SAML 2.0 IdP (PLT-030).\n # Feature-flagged: SSO routes only active when FeatureFlag(key='sso_authentication') is enabled.\n SSO_PROVIDERS = {\n \"azure\": {\n \"client_id\": os.environ.get(\"AZURE_AD_CLIENT_ID\", \"\"),\n \"client_secret\": os.environ.get(\"AZURE_AD_CLIENT_SECRET\", \"\"),\n \"server_metadata_url\": os.environ.get(\n \"AZURE_AD_METADATA_URL\",\n \"https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration\".format(\n tenant=os.environ.get(\"AZURE_AD_TENANT_ID\", \"common\")\n ),\n ),\n \"client_kwargs\": {\"scope\": \"openid email profile\"},\n },\n \"okta\": {\n \"client_id\": os.environ.get(\"OKTA_CLIENT_ID\", \"\"),\n \"client_secret\": os.environ.get(\"OKTA_CLIENT_SECRET\", \"\"),\n \"server_metadata_url\": os.environ.get(\"OKTA_METADATA_URL\", \"\"),\n \"client_kwargs\": {\"scope\": \"openid email profile\"},\n },\n }\n\n # SAML 2.0 IdP configuration (PLT-030)\n # Set SAML_IDP_SSO_URL + SAML_SP_ENTITY_ID to enable SAML alongside OIDC.\n # Supported IdPs: ADFS, PingFederate, Shibboleth, Okta (SAML), Azure AD (SAML).\n # Routes registered at /account/saml/login, /account/saml/acs, /account/saml/metadata.\n SAML_IDP_SSO_URL = os.environ.get(\"SAML_IDP_SSO_URL\", \"\")\n # IdP Entity ID / Issuer URI (validated against Assertion Issuer element)\n SAML_IDP_ENTITY_ID = os.environ.get(\"SAML_IDP_ENTITY_ID\", \"\")\n # IdP X.509 signing certificate — PEM body without -----BEGIN/END----- headers.\n # Required for production signature validation (via xmlsec / python3-saml).\n SAML_IDP_CERT = os.environ.get(\"SAML_IDP_CERT\", \"\")\n # SP Entity ID — typically the platform's base URL\n SAML_SP_ENTITY_ID = os.environ.get(\"SAML_SP_ENTITY_ID\", \"\")\n # SP ACS URL — leave empty to auto-derive from url_for('account.saml_acs')\n SAML_SP_ACS_URL = os.environ.get(\"SAML_SP_ACS_URL\", \"\")\n\n # Internationalization (S2-01) — date/number/currency formatting\n # Full string translation (gettext) is Phase 2.\n BABEL_DEFAULT_LOCALE = os.environ.get(\"BABEL_DEFAULT_LOCALE\", \"en\")\n BABEL_DEFAULT_TIMEZONE = os.environ.get(\"BABEL_DEFAULT_TIMEZONE\", \"UTC\")\n\n # Read Replica Configuration (ENH-012)\n # Set DATABASE_READ_REPLICA_URL to a replica DB connection string to offload\n # dashboard and portfolio GET queries from the primary. When not configured,\n # all queries use the default SQLALCHEMY_DATABASE_URI (no-op).\n DATABASE_READ_REPLICA_URL = os.environ.get(\"DATABASE_READ_REPLICA_URL\", \"\")\n USE_READ_REPLICA_FOR_DASHBOARD = (\n os.environ.get(\"USE_READ_REPLICA_FOR_DASHBOARD\", \"false\").lower() == \"true\"\n )\n\n # AI-originated CRUD approval gate (A95-008)\n # When true, AI CRUD endpoints return 202 pending_approval instead of writing directly.\n REQUIRE_AI_APPROVAL = os.environ.get(\"REQUIRE_AI_APPROVAL\", \"false\").lower() == \"true\"\n\n # North Star Navigation (NORTH-STAR-001)\n # Enterprise-grade navigation for Fortune 500 TOGAF/ArchiMate practitioners.\n # When enabled: renamed navigation items, persona-based filtering, hidden admin items.\n # Rollback: set to false and restart service (zero risk, instant revert).\n # ENABLED 2026-04-02: Quick wins from NORTH_STAR_ASSESSMENT.md Part 10\n ENABLE_NORTH_STAR_NAV = _env_bool(\"ENABLE_NORTH_STAR_NAV\", True)\n\n # North Star Navigation Phase 2 (NORTH-STAR-002)\n # Complete ArchiMate 3.2 layer-based navigation with all 55 element types.\n # When enabled: 6-section IA, layer-first architecture, Portfolio/Architecture separation.\n # Depends on: ENABLE_NORTH_STAR_NAV=True (Phase 1 must be active)\n # Rollback: set to false and restart service.\n ENABLE_NORTH_STAR_PHASE2 = _env_bool(\"ENABLE_NORTH_STAR_PHASE2\", False)\n\n # Page-aware AI guide is fail-closed by default and only activates when\n # this flag is explicitly enabled and an LLM provider is configured.\n AI_PAGE_GUIDE_ENABLED = _env_bool(\"AI_PAGE_GUIDE_ENABLED\", False)\n\n # File Upload Settings\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max file size\n ALLOWED_EXTENSIONS = {\n \"pdf\",\n \"docx\",\n \"doc\",\n \"xlsx\",\n \"xls\",\n \"png\",\n \"jpg\",\n \"jpeg\",\n \"vsdx\",\n \"txt\",\n \"csv\",\n \"md\",\n }\n UPLOAD_FOLDER = os.path.join(basedir, \"uploads\", \"documents\")\n\n @staticmethod\n def init_app(app):\n \"\"\"Base class hook — subclasses override to customize app initialization.\"\"\"\n return\n\n\nclass DevelopmentConfig(Config):\n DEBUG = True\n ASSETS_DEBUG = True\n\n # N+1 query detection — set SQLALCHEMY_ECHO=true in .env to enable query logging\n SQLALCHEMY_ECHO = os.environ.get(\"SQLALCHEMY_ECHO\", \"false\").lower() == \"true\"\n JIRA_AUTO_PUSH = os.environ.get(\"JIRA_AUTO_PUSH\", \"false\").lower() == \"true\"\n\n # PostgreSQL configuration (REQUIRED - validated at runtime)\n # Default to a local Postgres instance for development when DATABASE_URL is not set\n # Note: Default port is 5439 for local Windows PostgreSQL installation\n SQLALCHEMY_DATABASE_URI = os.environ.get(\n \"DATABASE_URL\",\n \"postgresql://postgres:postgres@127.0.0.1:5439/flask_app\", # secrets-safety-ok\n )\n\n # SERVER_NAME must NOT be hardcoded — Flask-WTF CSRF validates the Referer\n # header against SERVER_NAME, so a hardcoded value causes \"CSRF token is missing\"\n # when the browser omits the Referer header (privacy settings, extensions, etc.).\n # Only set when explicitly provided via environment variable.\n SERVER_NAME = os.environ.get(\"SERVER_NAME\") or None\n PREFERRED_URL_SCHEME = os.environ.get(\"PREFERRED_URL_SCHEME\", \"http\")\n APPLICATION_ROOT = os.environ.get(\"APPLICATION_ROOT\", \"/\")\n\n # PostgreSQL connection options (optimized for local development)\n SQLALCHEMY_ENGINE_OPTIONS = {\n \"pool_pre_ping\": True,\n \"pool_recycle\": 300,\n \"pool_size\": 5,\n \"max_overflow\": 10,\n \"pool_timeout\": 30,\n \"connect_args\": {\n \"client_encoding\": \"utf8\",\n \"options\": \"-c client_encoding=utf8\",\n },\n \"pool_reset_on_return\": \"commit\",\n }\n\n @classmethod\n def init_app(cls, app):\n # Ensure a sensible default exists for development (Postgres is the standard)\n if not app.config.get(\"SQLALCHEMY_DATABASE_URI\"):\n app.logger.warning(\n \"No DATABASE_URL provided; falling back to local Postgres default.\"\n )\n print(\n \"THIS APP IS IN DEBUG MODE. \\\n YOU SHOULD NOT SEE THIS IN PRODUCTION.\"\n )\n\n\nclass TestingConfig(Config):\n TESTING = True\n WTF_CSRF_ENABLED = False\n\n # PostgreSQL REQUIRED for tests (matches production behavior)\n # SQLite is NOT supported - tests must use PostgreSQL for consistency\n # Note: Default port is 5439 for local Windows PostgreSQL installation\n SQLALCHEMY_DATABASE_URI = os.environ.get(\n \"TEST_DATABASE_URL\",\n \"postgresql://postgres:postgres@127.0.0.1:5439/flask_test\", # secrets-safety-ok\n )\n\n # PostgreSQL connection options for testing\n SQLALCHEMY_ENGINE_OPTIONS = {\n \"pool_pre_ping\": True,\n \"pool_size\": 5,\n \"max_overflow\": 10,\n }\n\n @classmethod\n def init_app(cls, app):\n # Validate PostgreSQL is being used (not SQLite)\n db_uri = app.config.get(\"SQLALCHEMY_DATABASE_URI\", \"\")\n if \"sqlite\" in db_uri.lower():\n raise ValueError(\n \"TEST_DATABASE_URL must be PostgreSQL, not SQLite. \"\n \"Set TEST_DATABASE_URL to a PostgreSQL connection string. \"\n \"Example: postgresql://postgres:postgres@localhost:5432/flask_test\" # secrets-safety-ok\n )\n\n if not db_uri:\n app.logger.warning(\n \"No TEST_DATABASE_URL provided; using default local PostgreSQL test database.\"\n )\n\n print(\"THIS APP IS IN TESTING MODE. YOU SHOULD NOT SEE THIS IN PRODUCTION.\")\n\n\nclass ProductionConfig(Config):\n DEBUG = False\n USE_RELOADER = False\n\n # Session cookie security\n _cookie_secure_default = os.environ.get(\"PREFERRED_URL_SCHEME\", \"http\").lower() == \"https\"\n SESSION_COOKIE_SECURE = _env_bool(\"SESSION_COOKIE_SECURE\", _cookie_secure_default)\n SESSION_COOKIE_HTTPONLY = True\n SESSION_COOKIE_SAMESITE = \"Lax\"\n REMEMBER_COOKIE_SECURE = _env_bool(\"REMEMBER_COOKIE_SECURE\", _cookie_secure_default)\n REMEMBER_COOKIE_HTTPONLY = True\n\n # PostgreSQL for production (REQUIRED - validated at runtime)\n SQLALCHEMY_DATABASE_URI = os.environ.get(\"DATABASE_URL\")\n SSL_DISABLE = os.environ.get(\"SSL_DISABLE\", \"False\") == \"True\"\n JIRA_AUTO_PUSH = os.environ.get(\"JIRA_AUTO_PUSH\", \"false\").lower() == \"true\"\n\n # PostgreSQL connection options (optimized for production)\n SQLALCHEMY_ENGINE_OPTIONS = {\n \"pool_pre_ping\": True,\n \"pool_recycle\": 300,\n \"pool_size\": 20,\n \"max_overflow\": 30,\n \"pool_timeout\": 30,\n \"connect_args\": {\n \"client_encoding\": \"utf8\",\n \"options\": \"-c client_encoding=utf8\",\n },\n \"pool_reset_on_return\": \"commit\",\n }\n\n @classmethod\n def init_app(cls, app):\n # Validate DATABASE_URL is set\n if not app.config.get(\"SQLALCHEMY_DATABASE_URI\"):\n raise ValueError(\n \"DATABASE_URL environment variable is required. PostgreSQL must be configured for production.\"\n )\n\n Config.init_app(app)\n assert os.environ.get(\"SECRET_KEY\"), \"SECRET_KEY IS NOT SET!\"\n\n # Trust the nginx reverse proxy's forwarded headers (X-Forwarded-Proto /\n # -For) so request.scheme/host reflect the real client-facing protocol.\n # Without this, behind TLS the app still builds http:// external URLs\n # (breaking OAuth redirect URIs, email links) and treats requests as\n # insecure. nginx is the single trusted hop. Idempotent so a subclass\n # (Heroku/Unix) re-running init_app cannot double-wrap.\n if not getattr(app, \"_proxyfix_applied\", False):\n from werkzeug.middleware.proxy_fix import ProxyFix\n\n app.wsgi_app = ProxyFix(\n app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1\n )\n app._proxyfix_applied = True\n\n if HAS_RAYGUN and flask_raygun and app.config.get(\"RAYGUN_APIKEY\"):\n flask_raygun.Provider(app, app.config[\"RAYGUN_APIKEY\"]).attach()\n\n\nclass HerokuConfig(ProductionConfig):\n @classmethod\n def init_app(cls, app):\n # ProductionConfig.init_app applies ProxyFix (idempotently); no need to\n # re-wrap here.\n ProductionConfig.init_app(app)\n\n\nclass UnixConfig(ProductionConfig):\n @classmethod\n def init_app(cls, app):\n ProductionConfig.init_app(app)\n\n # Log to syslog\n import logging\n from logging.handlers import SysLogHandler\n\n syslog_handler = SysLogHandler()\n syslog_handler.setLevel(logging.WARNING)\n app.logger.addHandler(syslog_handler)\n\n\n# Currency Configuration System\nclass CurrencyConfig:\n \"\"\"Centralized currency configuration for the application\"\"\"\n\n DEFAULT_CURRENCY = os.environ.get(\"DEFAULT_CURRENCY\", \"GBP\")\n\n SUPPORTED_CURRENCIES = {\n \"GBP\": {\n \"symbol\": \"£\",\n \"code\": \"GBP\",\n \"position\": \"prefix\", # prefix or suffix\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"British Pound Sterling\",\n },\n \"USD\": {\n \"symbol\": \"$\",\n \"code\": \"USD\",\n \"position\": \"prefix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"United States Dollar\",\n },\n \"EUR\": {\n \"symbol\": \"€\",\n \"code\": \"EUR\",\n \"position\": \"suffix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \".\",\n \"name\": \"Euro\",\n },\n \"JPY\": {\n \"symbol\": \"¥\",\n \"code\": \"JPY\",\n \"position\": \"prefix\",\n \"decimal_places\": 0,\n \"thousands_separator\": \",\",\n \"name\": \"Japanese Yen\",\n },\n \"CAD\": {\n \"symbol\": \"C$\",\n \"code\": \"CAD\",\n \"position\": \"prefix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"Canadian Dollar\",\n },\n \"AUD\": {\n \"symbol\": \"A$\",\n \"code\": \"AUD\",\n \"position\": \"prefix\",\n \"decimal_places\": 2,\n \"thousands_separator\": \",\",\n \"name\": \"Australian Dollar\",\n },\n }\n\n @classmethod\n def get_currency_config(cls, currency_code=None):\n \"\"\"Get currency configuration for specified currency code\"\"\"\n if not currency_code:\n currency_code = cls.DEFAULT_CURRENCY\n\n config = cls.SUPPORTED_CURRENCIES.get(currency_code)\n if not config:\n # Fallback to default currency instead of crashing\n config = cls.SUPPORTED_CURRENCIES.get(cls.DEFAULT_CURRENCY)\n\n return config\n\n @classmethod\n def is_supported(cls, currency_code):\n \"\"\"Check if currency is supported\"\"\"\n return currency_code in cls.SUPPORTED_CURRENCIES\n\n @classmethod\n def get_all_supported_codes(cls):\n \"\"\"Get list of all supported currency codes\"\"\"\n return list(cls.SUPPORTED_CURRENCIES.keys())\n\n\nconfig = {\n \"development\": DevelopmentConfig,\n \"testing\": TestingConfig,\n \"production\": ProductionConfig,\n \"default\": DevelopmentConfig,\n \"heroku\": HerokuConfig,\n \"unix\": UnixConfig,\n}\n"}}}} |