Files
Nasim SadatandClaude Opus 5.5 d95e6e184a Superset wears the new design; site charts sit as boxes (PD-24 to PD-26)
Theme tokens for the slate surfaces and borders, the logo with the green Y, chart boxes at 16 px
with the card shadow. Full figures with the unit in the subheader; every layer of the tabs
transparent, so the site's charts sit as boxes in Firefox and Safari too. The Overview gets a Sites
filter and the period's scope returns to the time charts (PD-25). The embedded frame can shrink again.
A change to the image's inputs: Superset rebuilds on the next deploy.

Bands: Superset. Rules: 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 01:29:57 +01:00

282 lines
10 KiB
Python

"""Superset configuration for Petrolyst (ATOMA fuel monitoring).
Customization only, and no secrets: those are read from /run/secrets/<name>,
which stacks/fuel-monitoring/compose.yaml mounts from /opt/petrolyst/secrets/.
Baked into the image at /app/pythonpath/, which is on PYTHONPATH in
apache/superset:6.1.0, so Superset loads it without SUPERSET_CONFIG_PATH.
"""
import os
import pathlib
from copy import deepcopy
from flask_appbuilder.security.manager import AUTH_OAUTH
from sqlalchemy.engine.url import URL
from petrolyst_security import PetrolystSecurityManager
from superset import config as superset_defaults
def _read_secret(name: str, env_var: str | None = None) -> str:
"""Prefer /run/secrets/<name>; fall back to an env var for local dev.
Uses rstrip('\r\n') (not strip()) so passwords or keys that legitimately
contain leading/trailing spaces are not silently mutated. Raises on empty
values so boot fails loudly instead of accepting a blank secret.
"""
path = pathlib.Path(f"/run/secrets/{name}")
if path.is_file():
value = path.read_text().rstrip("\r\n")
else:
value = os.environ[env_var or name.upper()]
if not value:
raise RuntimeError(f"secret {name!r} must not be empty")
return value
SECRET_KEY = _read_secret("superset_secret_key", "SUPERSET_SECRET_KEY")
SQLALCHEMY_DATABASE_URI = URL.create(
"postgresql+psycopg2",
username="superset",
password=_read_secret("superset_db_password", "SUPERSET_DB_PASSWORD"),
host=os.environ.get("SUPERSET_DB_HOST", "superset-db"),
port=5432,
database="superset",
).render_as_string(hide_password=False)
# --- The data: fuel-db ----------------------------------------------------------
# Superset reads fuel-db directly as the read-only role fuel_ro;
# superset-init registers that connection, "Fuel", without a password. Passwords of data
# connections come from secret files at connect time, so none is kept in Superset's own
# database. A connection added in the UI gets no password this way.
_DATABASE_PASSWORD_SECRETS = {"fuel_ro": "fuel_db_ro_password"}
def _database_password(url: URL) -> str | None:
secret = _DATABASE_PASSWORD_SECRETS.get(url.username or "")
return _read_secret(secret) if secret else None
SQLALCHEMY_CUSTOM_PASSWORD_STORE = _database_password
# --- Sign-in through Keycloak (client "petrolyst") ----------------------------
# Users and roles are managed in Keycloak; there is no local admin account.
# Break-glass access: `superset fab ...` inside the container on the server.
KEYCLOAK_REALM_URL = os.environ.get(
"KEYCLOAK_REALM_URL", "https://iam.thedigitalbridge.io/realms/petrolyst"
).rstrip("/")
AUTH_TYPE = AUTH_OAUTH
CUSTOM_SECURITY_MANAGER = PetrolystSecurityManager
OAUTH_PROVIDERS = [
{
"name": "keycloak",
"icon": "fa-key",
"token_key": "access_token",
"remote_app": {
"client_id": os.environ.get("KEYCLOAK_CLIENT_ID", "petrolyst"),
"client_secret": _read_secret(
"keycloak_petrolyst_client_secret", "KEYCLOAK_CLIENT_SECRET"
),
"server_metadata_url": f"{KEYCLOAK_REALM_URL}/.well-known/openid-configuration",
"api_base_url": f"{KEYCLOAK_REALM_URL}/protocol/openid-connect/",
"client_kwargs": {
"scope": "openid email profile",
"code_challenge_method": "S256",
},
},
}
]
# Keys are Keycloak client roles of "petrolyst" (claim petrolyst_roles).
AUTH_ROLES_MAPPING = {
"superset-admin": ["Admin"],
"superset-editor": ["Alpha"],
"superset-viewer": ["Gamma"],
# Customers' users have a role of client "petrolyst-app", not of this one: refused here.
}
AUTH_ROLES_SYNC_AT_LOGIN = True
AUTH_USER_REGISTRATION = True
# Public has no permissions. A Keycloak user without a mapped role never gets
# this far: PetrolystSecurityManager refuses the sign-in and makes no account.
AUTH_USER_REGISTRATION_ROLE = "Public"
# Viewers (Gamma) see a dashboard once it lists their role.
# FILTERBAR_CLOSED_BY_DEFAULT: a dashboard's filter column opens collapsed, a
# narrow strip with an expand button (owner, 29 Oct); ?expand_filters=true
# opens it from a link.
FEATURE_FLAGS = {"DASHBOARD_RBAC": True, "FILTERBAR_CLOSED_BY_DEFAULT": True}
# deck.gl maps: OpenStreetMap raster tiles only. The default list adds Mapbox
# styles, which fail without MAPBOX_API_KEY. The default CSP already allows the
# tile host.
DECKGL_BASE_MAP = [
["tile://https://tile.openstreetmap.org/{z}/{x}/{y}.png", "OpenStreetMap"],
]
# --- Behind Traefik -----------------------------------------------------------
ENABLE_PROXY_FIX = True
PREFERRED_URL_SCHEME = "https"
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
TALISMAN_CONFIG = deepcopy(superset_defaults.TALISMAN_CONFIG)
TALISMAN_CONFIG["session_cookie_secure"] = True
# --- Redis: caches and rate limits (no Celery, so no results backend) ---------
REDIS_URL = os.environ.get("REDIS_URL", "redis://redis:6379")
CACHE_CONFIG = {
"CACHE_TYPE": "RedisCache",
"CACHE_DEFAULT_TIMEOUT": 300,
"CACHE_KEY_PREFIX": "superset_cache_",
"CACHE_REDIS_URL": f"{REDIS_URL}/0",
}
DATA_CACHE_CONFIG = {
"CACHE_TYPE": "RedisCache",
"CACHE_DEFAULT_TIMEOUT": 3600,
"CACHE_KEY_PREFIX": "superset_data_",
"CACHE_REDIS_URL": f"{REDIS_URL}/0",
}
FILTER_STATE_CACHE_CONFIG = {
"CACHE_TYPE": "RedisCache",
"CACHE_DEFAULT_TIMEOUT": 86400,
"CACHE_KEY_PREFIX": "superset_filter_",
"CACHE_REDIS_URL": f"{REDIS_URL}/0",
}
EXPLORE_FORM_DATA_CACHE_CONFIG = {
"CACHE_TYPE": "RedisCache",
"CACHE_DEFAULT_TIMEOUT": 86400,
"CACHE_KEY_PREFIX": "superset_explore_",
"CACHE_REDIS_URL": f"{REDIS_URL}/0",
}
RATELIMIT_STORAGE_URI = f"{REDIS_URL}/1"
# --- Petrolyst look and feel --------------------------------------------------
# Values from petrolyst-web/design/brand/07_UI_Tokens/petrolyst_tokens.css.
# Assets live in assets/branding/ and are served from /static/assets/branding/.
BRANDING = "/static/assets/branding"
APP_NAME = "Petrolyst"
APP_ICON = f"{BRANDING}/petrolyst_wordmark_fullcolor.svg"
FAVICONS = [
{"href": f"{BRANDING}/favicon.svg", "type": "image/svg+xml"},
{"href": f"{BRANDING}/favicon.ico", "type": "image/x-icon"},
{"href": f"{BRANDING}/petrolyst_icon_180.png", "rel": "apple-touch-icon"},
]
THEME_DEFAULT = deepcopy(superset_defaults.THEME_DEFAULT)
THEME_DEFAULT["token"].update(
{
"brandAppName": APP_NAME,
"brandLogoAlt": "Petrolyst",
"brandLogoUrl": APP_ICON,
"brandLogoHref": "/",
# The wordmark alone (owner, 29 Oct): in the stacked logo, at 40px, its
# letters were about 5px high. Its own Y carries the brand accent. 24px
# (letters about 20px) and the margin fill the 60px navbar height. On the
# left, 15px more than the navbar's 16px: the P (1.4px inside the image)
# lines up with the dashboard's content at 32px.
"brandLogoHeight": "24px",
"brandLogoMargin": "18px 0 18px 15px",
# Action teal, not the raw brand teal #009688: the brand guide keeps
# raw teal off small text and controls.
"colorPrimary": "#00695C",
"colorLink": "#00695C",
"colorSuccess": "#0F7B6C",
"colorWarning": "#B45309",
"colorError": "#B3261E",
"colorInfo": "#2563EB",
"colorTextBase": "#0B1D33",
"colorBgLayout": "#F7F9FA",
"colorBorder": "#DDE5E9",
"borderRadius": 8,
"fontFamily": "Inter, system-ui, sans-serif",
}
)
THEME_DARK = deepcopy(THEME_DEFAULT)
THEME_DARK["algorithm"] = "dark"
THEME_DARK["token"].update(
{
"brandLogoUrl": f"{BRANDING}/petrolyst_wordmark_white_y.svg",
"colorPrimary": "#35C5B6",
"colorLink": "#35C5B6",
"colorSuccess": "#35C5B6",
"colorWarning": "#F59E0B",
"colorError": "#FF6B6B",
"colorInfo": "#60A5FA",
"colorTextBase": "#F8FAFC",
"colorBgBase": "#1F2A33",
"colorBgLayout": "#1F2A33",
"colorBgContainer": "#2A3945",
"colorBorder": "#76828E",
"colorEditorSelection": superset_defaults.THEME_DARK["token"][
"colorEditorSelection"
],
}
)
# Chart colours. Validated with the dataviz skill's validator on both chart
# surfaces (#FFFFFF light, #2A3945 dark, PD-26): all slots in both lightness bands and
# >= 3:1; worst adjacent CVD dE 13.7, normal-vision dE 26.2; the first three
# slots also pass all-pairs (scatter, maps). Brand teal leads; brand cyan
# #00B6E6 is snapped to #0097C4 because it is too light for the dark surface
# and below 3:1 on white. The other hues come from the validated reference set.
# Status meaning (warning, critical) never rides on these colours.
EXTRA_CATEGORICAL_COLOR_SCHEMES = [
{
"id": "petrolyst",
"description": "Petrolyst categorical palette, brand teal first",
"label": "Petrolyst",
"isDefault": True,
"colors": [
"#009688", # brand teal
"#d95926", # orange
"#9085e9", # violet
"#c98500", # amber
"#0097C4", # brand cyan, snapped
"#e34948", # red
"#3987e5", # blue
"#d55181", # magenta
],
}
]
# One hue, light to dark; the lightest step means "near zero" and may recede.
EXTRA_SEQUENTIAL_COLOR_SCHEMES = [
{
"id": "petrolyst_teal",
"description": "Petrolyst teal, light to dark",
"label": "Petrolyst Teal",
"isDiverging": False,
"isDefault": True,
"colors": [
"#E6F4F2",
"#B3DED8",
"#7FC8BE",
"#4CB2A4",
"#009688",
"#00695C",
"#004D44",
],
},
{
"id": "petrolyst_navy",
"description": "Petrolyst navy, light to dark",
"label": "Petrolyst Navy",
"isDiverging": False,
"isDefault": False,
"colors": [
"#E8EEF5",
"#C3D1E2",
"#8FA7C4",
"#5C7DA6",
"#2F5585",
"#183A63",
"#0B1D33",
],
},
]