agnes-the-ai-analyst/webapp/config.py
Petr d438438e33 Add configurable white-label theming via instance.yaml
Extend theming from 3 CSS variables (primary colors only) to 14
configurable properties covering colors, fonts, borders, and shape.
All values are optional with sensible defaults.

- New _theme.html include replaces duplicated inline injection
- Wire theme include into all 7 templates (base, login, dashboard,
  catalog, admin_tables, activity_center, corporate_memory)
- Conditional font loading: skip default Inter when custom font_url set
- Config.theme_overrides() classmethod generates CSS variable dict
- Visual theme-reference.html guide for instance configurators
- Document all theme keys in instance.yaml.example
2026-03-11 13:58:58 +01:00

186 lines
17 KiB
Python

"""
Configuration for the webapp.
All sensitive values are loaded from environment variables.
Instance-specific branding is loaded from config/instance.yaml.
"""
import os
from pathlib import Path
def _load_instance_config():
"""Load instance config with graceful fallback for development."""
try:
from config.loader import load_instance_config
return load_instance_config()
except (FileNotFoundError, ImportError, Exception) as e:
import logging
logging.getLogger(__name__).warning(f"Instance config not found, using defaults: {e}")
return {}
def _get(config, *keys, default=""):
"""Get nested config value by traversing keys."""
value = config
for key in keys:
if not isinstance(value, dict) or key not in value:
return default
value = value[key]
return value if value is not None else default
_instance = _load_instance_config()
class Config:
"""Flask configuration from environment variables and instance config."""
# Flask
SECRET_KEY = os.environ.get("WEBAPP_SECRET_KEY", "dev-secret-key-change-me")
DEBUG = os.environ.get("FLASK_DEBUG", "false").lower() == "true"
# Google OAuth
GOOGLE_CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID", "")
GOOGLE_CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET", "")
# Domain restriction for login (loaded from instance config)
# Supports single domain string or comma-separated list
ALLOWED_DOMAIN = _get(_instance, "auth", "allowed_domain", default="")
ALLOWED_DOMAINS = [
d.strip().lower()
for d in _get(_instance, "auth", "allowed_domain", default="").split(",")
if d.strip()
]
# Password authentication for external users (whitelisted emails)
ALLOWED_EMAILS = [
e.strip().lower()
for e in os.environ.get("ALLOWED_EMAILS", "").split(",")
if e.strip()
]
PASSWORD_USERS_FILE = Path(
os.environ.get("PASSWORD_USERS_FILE", "/data/auth/password_users.json")
)
# SMTP email delivery (for magic link auth)
SMTP_HOST = os.environ.get("SMTP_HOST", "")
SMTP_PORT = int(os.environ.get("SMTP_PORT", "587"))
SMTP_USER = os.environ.get("SMTP_USER", "")
SMTP_PASSWORD = os.environ.get("SMTP_PASSWORD", "")
SMTP_FROM = os.environ.get("SMTP_FROM",
os.environ.get("SMTP_USER",
_get(_instance, "email", "from_address", default="noreply@example.com")))
SMTP_USE_TLS = os.environ.get("SMTP_USE_TLS", "true").lower() == "true"
# SendGrid email service (legacy, for password auth)
SENDGRID_API_KEY = os.environ.get("SENDGRID_API_KEY", "")
EMAIL_FROM_ADDRESS = os.environ.get("EMAIL_FROM_ADDRESS",
_get(_instance, "email", "from_address", default="noreply@example.com"))
EMAIL_FROM_NAME = os.environ.get("EMAIL_FROM_NAME",
_get(_instance, "email", "from_name", default="AI Data Analyst"))
# Token expiry times (seconds)
SETUP_TOKEN_EXPIRY = 86400 # 24 hours
RESET_TOKEN_EXPIRY = 3600 # 1 hour
# Server info for SSH connection instructions (loaded from instance config)
SERVER_HOST = os.environ.get("SERVER_HOST",
_get(_instance, "server", "host", default=""))
SERVER_HOSTNAME = os.environ.get("SERVER_HOSTNAME",
_get(_instance, "server", "hostname", default=""))
# Session config
SESSION_TYPE = "filesystem"
SESSION_PERMANENT = False
PERMANENT_SESSION_LIFETIME = 3600 # 1 hour
# Desktop app JWT authentication
DESKTOP_JWT_SECRET = os.environ.get("DESKTOP_JWT_SECRET", "")
DESKTOP_JWT_EXPIRY_DAYS = 30
DESKTOP_JWT_REFRESH_GRACE_DAYS = 7
DESKTOP_JWT_ISSUER = _get(_instance, "desktop", "jwt_issuer", default="data-analyst")
DESKTOP_URL_SCHEME = _get(_instance, "desktop", "url_scheme", default="data-analyst")
# Instance branding (for templates)
INSTANCE_NAME = _get(_instance, "instance", "name", default="AI Data Analyst")
INSTANCE_SUBTITLE = _get(_instance, "instance", "subtitle", default="")
INSTANCE_COPYRIGHT = _get(_instance, "instance", "copyright", default="")
# Logo SVG (full <svg> element, rendered with |safe in templates)
# Default: Keboola logo; override in instance.yaml with instance.logo_svg
LOGO_SVG = _get(_instance, "instance", "logo_svg", default=(
'<svg width="120" height="30" viewBox="0 0 395 100" xmlns="http://www.w3.org/2000/svg">'
'<path d="M390.16321,40.175397 C393.472414,43.4361562 395,48.2443368 395,54.1631395 L395,76.4805472 C395,79.3112789 392.794993,81.45803 389.993855,81.45803 C387.019974,81.45803 384.984322,79.39593 384.984322,77.0798767 L384.984322,75.3631531 C381.929151,79.0539397 377.25833,81.9727085 370.382501,81.9727085 C361.979087,81.9727085 354.507127,77.0798767 354.507127,67.9815799 L354.507127,67.8122778 C354.507127,58.0266143 362.063765,53.2218197 373.014284,53.2218197 C378.023816,53.2218197 381.587053,53.9938374 385.069,55.1078455 L385.069,53.9938374 C385.069,47.5569702 381.163665,44.1235228 373.949126,44.1235228 C370.04379,44.1235228 366.819264,44.8108895 364.014739,45.9248976 C363.421995,46.0975858 362.913929,46.1822368 362.402475,46.1822368 C360.028113,46.1822368 358.073752,44.296211 358.073752,41.8921207 C358.073752,40.0027088 359.347304,38.3740223 360.87489,37.7746927 C365.118936,36.1426201 369.447659,35.1132631 375.307356,35.1132631 C382.013829,35.1132631 387.019974,36.9146379 390.16321,40.175397 Z M385.238356,64.7208208 L385.238356,61.6327498 C382.606573,60.6033928 379.124626,59.827989 375.053323,59.827989 C368.431527,59.827989 364.526192,62.6621068 364.526192,67.3822504 L364.526192,67.5515525 C364.526192,71.9297058 368.346849,74.4184472 373.268317,74.4184472 C380.059468,74.4184472 385.238356,70.4703213 385.238356,64.7208208 Z M343.983384,17.9494125 C346.869199,17.9494125 349.162271,20.2654658 349.162271,23.0961975 L349.162271,76.307859 C349.162271,79.2266278 346.869199,81.45803 343.983384,81.45803 C341.182246,81.45803 338.889174,79.2266278 338.889174,76.307859 L338.889174,23.0961975 C338.889174,20.2654658 341.097568,17.9494125 343.983384,17.9494125 Z M309.096174,34.7678868 C322.847832,34.7678868 332.948187,45.325568 332.948187,58.2839535 L332.948187,58.4566417 C332.948187,71.3303762 322.763154,82.0573596 308.926819,82.0573596 C295.259839,82.0573596 285.156097,71.4996783 285.156097,58.6293299 L285.156097,58.4566417 C285.156097,45.4982562 295.344517,34.7678868 309.096174,34.7678868 Z M322.678476,58.6293299 L322.678476,58.4566417 C322.678476,50.472353 316.991522,43.8661836 308.926819,43.8661836 C300.69276,43.8661836 295.429195,50.3910879 295.429195,58.2839535 L295.429195,58.4566417 C295.429195,66.3528934 301.116148,72.9624488 309.096174,72.9624488 C317.414911,72.9624488 322.678476,66.4375444 322.678476,58.6293299 Z M258.418269,34.7678868 C272.169926,34.7678868 282.270281,45.325568 282.270281,58.2839535 L282.270281,58.4566417 C282.270281,71.3303762 272.085248,82.0573596 258.248913,82.0573596 C244.581934,82.0573596 234.478191,71.4996783 234.478191,58.6293299 L234.478191,58.4566417 C234.478191,45.4982562 244.666611,34.7678868 258.418269,34.7678868 Z M272.000571,58.6293299 L272.000571,58.4566417 C272.000571,50.472353 266.313617,43.8661836 258.248913,43.8661836 C250.014854,43.8661836 244.751289,50.3910879 244.751289,58.2839535 L244.751289,58.4566417 C244.751289,66.3528934 250.438243,72.9624488 258.418269,72.9624488 C266.737005,72.9624488 272.000571,66.4375444 272.000571,58.6293299 Z M210.626179,34.7678868 C221.15331,34.7678868 231.426407,43.1788169 231.426407,58.2839535 L231.426407,58.4566417 C231.426407,73.4737412 221.237987,81.9727085 210.626179,81.9727085 C203.157606,81.9727085 198.490172,78.1938848 195.346936,73.9918058 L195.346936,76.307859 C195.346936,79.1385907 193.057251,81.45803 190.168048,81.45803 C187.36691,81.45803 185.077225,79.1385907 185.077225,76.307859 L185.077225,23.0961975 C185.077225,20.1774286 187.282232,17.9494125 190.168048,17.9494125 C193.057251,17.9494125 195.346936,20.1774286 195.346936,23.0961975 L195.346936,43.266854 C198.659527,38.5467105 203.326962,34.7678868 210.626179,34.7678868 Z M220.983954,58.4566417 L220.983954,58.2839535 C220.983954,49.5310331 215.127645,43.7781465 208.167139,43.7781465 C201.206632,43.7781465 195.092903,49.6156841 195.092903,58.2839535 L195.092903,58.4566417 C195.092903,67.1249111 201.206632,72.9624488 208.167139,72.9624488 C215.212323,72.9624488 220.983954,67.3822504 220.983954,58.4566417 Z M158.339397,34.7678868 C172.599121,34.7678868 179.644305,46.6122642 179.644305,57.0819084 C179.644305,60.0006772 177.439297,62.0593912 174.807515,62.0593912 L146.708069,62.0593912 C147.812266,69.4409643 152.991154,73.5617783 159.61295,73.5617783 C163.941673,73.5617783 167.335555,72.0177429 170.221371,69.6136525 C170.986857,69.014323 171.664279,68.6689466 172.853154,68.6689466 C175.146226,68.6689466 176.927844,70.4703213 176.927844,72.8744117 C176.927844,74.1611079 176.3351,75.278502 175.569614,76.0505198 C171.494923,79.7413063 166.404101,82.0573596 159.440207,82.0573596 C146.454036,82.0573596 136.438359,72.5324214 136.438359,58.5412928 L136.438359,58.3719907 C136.438359,45.4102191 145.519194,34.7678868 158.339397,34.7678868 Z M146.623392,55.1958826 L169.628627,55.1958826 C168.947818,48.49829 165.04587,43.266854 158.254719,43.266854 C151.971635,43.266854 147.558233,48.1562997 146.623392,55.1958826 Z M114.621998,47.384282 L134.65674,72.5324214 C135.503517,73.6464294 136.099648,74.6757864 136.099648,76.307859 C136.099648,79.2266278 133.806576,81.45803 130.836082,81.45803 C128.797044,81.45803 127.523491,80.428673 126.422681,78.9692886 L107.322781,54.3358277 L97.645814,63.7761149 L97.645814,76.2198219 C97.645814,79.1385907 95.3527421,81.45803 92.4669263,81.45803 C89.4964329,81.45803 87.2033609,79.1385907 87.2033609,76.2198219 L87.2033609,25.7576271 C87.2033609,22.8388582 89.4964329,20.522805 92.4669263,20.522805 C95.3527421,20.522805 97.645814,22.8388582 97.645814,25.7576271 L97.645814,51.1631057 L125.82655,22.4968679 C127.015425,21.2067856 128.288977,20.522805 130.155274,20.522805 C133.04109,20.522805 134.995451,22.8388582 134.995451,25.4156367 C134.995451,27.0443233 134.314642,28.2463685 133.129154,29.3637626 L114.621998,47.384282 Z M29.0196247,62.5097349 C15.2069994,62.5097349 4.64599757,45.6777165 4.64599757,24.3591914 C4.64599757,3.04405242 13.4457034,0 29.0196247,0 C44.5935459,0 52.6311525,3.04405242 52.6311525,24.3591914 C52.6311525,45.6777165 41.5790201,62.5097349 29.0196247,62.5097349 Z M56.7160044,81.9659364 C57.8506855,83.8519622 57.207135,86.2865269 55.2832579,87.397149 C54.6363203,87.7696137 53.9284148,87.9456879 53.2306706,87.9456879 C51.8453435,87.9456879 50.4938876,87.2481631 49.7385625,85.9919412 C48.4345261,83.831646 47.1000056,82.2943826 45.5961298,80.8891748 C44.092254,79.4907392 42.3851517,78.2379034 40.4307905,76.7785189 C39.4756262,76.070836 38.5238489,75.4579623 37.5754587,74.9365117 C37.6872333,75.1396743 37.7990079,75.3462229 37.9141695,75.5595436 C40.6577268,80.7198727 43.4893488,87.8813531 43.5028972,96.03833 C43.5028972,98.2257136 41.6907946,100 39.4553035,100 C37.2164253,100 35.4043227,98.2257136 35.4043227,96.03833 C35.4178711,89.8350997 33.1959285,83.8688924 30.8520499,79.4467206 C30.0933378,78.0042664 29.3278515,76.7311143 28.6368815,75.6678969 C27.9831697,76.6803237 27.2583287,77.8823689 26.5334876,79.236786 C24.1523509,83.679274 21.8558919,89.7402905 21.8694403,96.03833 C21.8694403,98.2257136 20.0573377,100 17.8218466,100 C15.5863555,100 13.7742529,98.2257136 13.7742529,96.03833 C13.7878013,87.8813531 16.6160362,80.7198727 19.3629806,75.5595436 C19.4747551,75.3462229 19.5865297,75.1396743 19.7016913,74.9365117 C18.7499141,75.4579623 17.8015239,76.070836 16.8463595,76.7785189 C14.8886113,78.2379034 13.181509,79.4907392 11.6810203,80.8891748 C10.1771445,82.2943826 8.83923692,83.831646 7.53520045,85.9953273 C6.40051937,87.8813531 3.91776941,88.507771 1.99389223,87.397149 C0.0666279455,86.2865269 -0.573535412,83.8519622 0.561145671,81.9659364 C2.25469953,79.1453628 4.15825406,76.944435 6.1092281,75.1362882 C8.06358925,73.3247554 10.0314988,71.8958453 11.9418276,70.4669353 C12.8461853,69.7897267 13.7742529,69.1666949 14.722643,68.6012257 C13.7742529,68.0357566 12.8461853,67.4093387 11.9418276,66.7321302 C7.57245863,63.471371 3.77551089,59.0729015 0.561145671,53.7331121 C-0.573535412,51.8437003 0.0666279455,49.4125216 1.99389223,48.3018996 C3.91776941,47.1912776 6.40051937,47.8176955 7.53520045,49.7037213 C10.319403,54.3324417 13.4795745,57.9148749 16.8429724,60.4205465 C20.4502422,63.0921342 23.9931568,64.4160769 27.6376847,64.6260116 C27.9696213,64.6158535 28.3049449,64.6056953 28.6368815,64.6056953 C28.9722052,64.6056953 29.3041417,64.6158535 29.6360783,64.6260116 C33.2806062,64.4160769 36.8269079,63.0921342 40.4307905,60.4205465 C43.7941885,57.9148749 46.9577471,54.3324417 49.7385625,49.7037213 C50.8766307,47.8176955 53.3559936,47.1912776 55.2832579,48.3018996 C57.207135,49.4125216 57.8506855,51.8437003 56.7160044,53.7297261 C53.5016392,59.0729015 49.7013043,63.471371 45.3319354,66.7321302 C44.4275776,67.4093387 43.4995101,68.0357566 42.5545071,68.6012257 C43.4995101,69.1666949 44.4275776,69.7897267 45.3319354,70.4669353 C47.2456513,71.8958453 49.2101737,73.3247554 51.167922,75.1362882 C53.118896,76.944435 55.0190635,79.1453628 56.7160044,81.9659364 Z M23.3123482,48.6845224 C21.0768571,48.6845224 19.2647545,50.4621948 19.2647545,52.6495784 C19.2647545,54.8437341 21.0768571,56.6180205 23.3123482,56.6180205 C25.5478393,56.6180205 27.3599419,54.8437341 27.3599419,52.6495784 C27.3599419,50.4621948 25.5478393,48.6845224 23.3123482,48.6845224 Z M33.9614148,48.6845224 C31.7259237,48.6845224 29.9138211,50.4621948 29.9138211,52.6495784 C29.9138211,54.8437341 31.7259237,56.6180205 33.9614148,56.6180205 C36.1969059,56.6180205 38.0090085,54.8437341 38.0090085,52.6495784 C38.0090085,50.4621948 36.1969059,48.6845224 33.9614148,48.6845224 Z" fill="#0073D1" fill-rule="nonzero"/>'
'</svg>'
))
# Theme (optional overrides from instance config)
THEME_PRIMARY = _get(_instance, "theme", "primary", default="")
THEME_PRIMARY_DARK = _get(_instance, "theme", "primary_dark", default="")
THEME_PRIMARY_LIGHT = _get(_instance, "theme", "primary_light", default="")
THEME_TEXT_PRIMARY = _get(_instance, "theme", "text_primary", default="")
THEME_TEXT_SECONDARY = _get(_instance, "theme", "text_secondary", default="")
THEME_BACKGROUND = _get(_instance, "theme", "background", default="")
THEME_SURFACE = _get(_instance, "theme", "surface", default="")
THEME_BORDER = _get(_instance, "theme", "border", default="")
THEME_FONT_PRIMARY = _get(_instance, "theme", "font_primary", default="")
THEME_FONT_URL = _get(_instance, "theme", "font_url", default="")
THEME_RADIUS = _get(_instance, "theme", "radius", default="")
THEME_SUCCESS = _get(_instance, "theme", "success", default="")
THEME_WARNING = _get(_instance, "theme", "warning", default="")
THEME_ERROR = _get(_instance, "theme", "error", default="")
# Auth providers to disable (list of provider names, e.g., ["email", "password"])
AUTH_DISABLED_PROVIDERS = _get(_instance, "auth", "disabled_providers", default=[])
# Telegram bot
TELEGRAM_BOT_USERNAME = _get(_instance, "telegram", "bot_username", default="")
# Notification images directory
NOTIFICATION_IMAGES_DIR = "/tmp"
# Jira connector (optional - loaded from connectors/jira/)
# These remain here for backward compatibility; the Jira connector
# reads them from this Config class.
JIRA_ENABLED = os.environ.get("JIRA_DOMAIN", "") != ""
JIRA_WEBHOOK_SECRET = os.environ.get("JIRA_WEBHOOK_SECRET", "")
JIRA_DOMAIN = os.environ.get("JIRA_DOMAIN", "")
JIRA_EMAIL = os.environ.get("JIRA_EMAIL", "")
JIRA_API_TOKEN = os.environ.get("JIRA_API_TOKEN", "")
JIRA_SLA_EMAIL = os.environ.get("JIRA_SLA_EMAIL", "")
JIRA_SLA_API_TOKEN = os.environ.get("JIRA_SLA_API_TOKEN", "")
JIRA_CLOUD_ID = os.environ.get("JIRA_CLOUD_ID", "")
JIRA_DATA_DIR = Path(os.environ.get("JIRA_DATA_DIR", "/data/src_data/raw/jira"))
@classmethod
def theme_overrides(cls) -> dict:
"""Return non-empty theme CSS variable overrides."""
mapping = {
"--primary": cls.THEME_PRIMARY,
"--primary-dark": cls.THEME_PRIMARY_DARK,
"--primary-light": cls.THEME_PRIMARY_LIGHT,
"--text-primary": cls.THEME_TEXT_PRIMARY,
"--text-secondary": cls.THEME_TEXT_SECONDARY,
"--background": cls.THEME_BACKGROUND,
"--surface": cls.THEME_SURFACE,
"--border": cls.THEME_BORDER,
"--font-primary": cls.THEME_FONT_PRIMARY,
"--radius-md": cls.THEME_RADIUS,
"--success": cls.THEME_SUCCESS,
"--warning": cls.THEME_WARNING,
"--error": cls.THEME_ERROR,
}
return {k: v for k, v in mapping.items() if v}
@classmethod
def validate(cls) -> list[str]:
"""Validate that required configuration is present."""
errors = []
if not cls.GOOGLE_CLIENT_ID:
errors.append("GOOGLE_CLIENT_ID is not set")
if not cls.GOOGLE_CLIENT_SECRET:
errors.append("GOOGLE_CLIENT_SECRET is not set")
if cls.SECRET_KEY == "dev-secret-key-change-me":
errors.append("WEBAPP_SECRET_KEY should be set to a secure random value")
return errors