feat: Add application-level settings for CSS color management
Some checks failed
Run Tests / test (push) Failing after 1m51s

- Introduced a new table `application_setting` to store configurable application options.
- Implemented functions to manage CSS color settings, including loading, updating, and reading environment overrides.
- Added a new settings view to render and manage theme colors.
- Updated UI to include a settings page with theme color management and environment overrides display.
- Enhanced CSS styles for the settings page and sidebar navigation.
- Created unit and end-to-end tests for the new settings functionality and CSS management.
This commit is contained in:
2025-10-25 19:20:52 +02:00
parent e74ec79cc9
commit 5b1322ddbc
24 changed files with 1336 additions and 35 deletions

View File

@@ -7,6 +7,7 @@ UI_ROUTES = [
("/ui/dashboard", "Dashboard · CalMiner", "Operations Overview"),
("/ui/scenarios", "Scenario Management · CalMiner", "Create a New Scenario"),
("/ui/parameters", "Process Parameters · CalMiner", "Scenario Parameters"),
("/ui/settings", "Settings · CalMiner", "Settings"),
("/ui/costs", "Costs · CalMiner", "Cost Overview"),
("/ui/consumption", "Consumption · CalMiner", "Consumption Tracking"),
("/ui/production", "Production · CalMiner", "Production Output"),
@@ -27,3 +28,45 @@ def test_ui_pages_load_correctly(page: Page, url: str, title: str, heading: str)
heading_locator = page.locator(
f"h1:has-text('{heading}'), h2:has-text('{heading}')")
expect(heading_locator.first).to_be_visible()
def test_settings_theme_form_interaction(page: Page):
page.goto("/ui/settings")
expect(page).to_have_title("Settings · CalMiner")
env_rows = page.locator("#theme-env-overrides tbody tr")
disabled_inputs = page.locator(
"#theme-settings-form input.color-value-input[disabled]")
env_row_count = env_rows.count()
disabled_count = disabled_inputs.count()
assert disabled_count == env_row_count
color_input = page.locator(
"#theme-settings-form input[name='--color-primary']")
expect(color_input).to_be_visible()
expect(color_input).to_be_enabled()
original_value = color_input.input_value()
candidate_values = ("#114455", "#225566")
new_value = candidate_values[0] if original_value != candidate_values[0] else candidate_values[1]
color_input.fill(new_value)
page.click("#theme-settings-form button[type='submit']")
feedback = page.locator("#theme-settings-feedback")
expect(feedback).to_contain_text("updated successfully")
computed_color = page.evaluate(
"() => getComputedStyle(document.documentElement).getPropertyValue('--color-primary').trim()"
)
assert computed_color.lower() == new_value.lower()
page.reload()
expect(color_input).to_have_value(new_value)
color_input.fill(original_value)
page.click("#theme-settings-form button[type='submit']")
expect(feedback).to_contain_text("updated successfully")
page.reload()
expect(color_input).to_have_value(original_value)

View File

@@ -34,6 +34,7 @@ TestingSessionLocal = sessionmaker(
def setup_database() -> Generator[None, None, None]:
# Ensure all model metadata is registered before creating tables
from models import (
application_setting,
capex,
consumption,
distribution,
@@ -52,6 +53,7 @@ def setup_database() -> Generator[None, None, None]:
distribution,
equipment,
maintenance,
application_setting,
opex,
parameters,
production_output,

View File

@@ -0,0 +1,53 @@
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from services import settings as settings_service
@pytest.mark.usefixtures("db_session")
def test_read_css_settings_reflects_env_overrides(
api_client: TestClient, monkeypatch: pytest.MonkeyPatch
) -> None:
env_var = settings_service.css_key_to_env_var("--color-background")
monkeypatch.setenv(env_var, "#123456")
response = api_client.get("/api/settings/css")
assert response.status_code == 200
body = response.json()
assert body["variables"]["--color-background"] == "#123456"
assert body["env_overrides"]["--color-background"] == "#123456"
assert any(
source["env_var"] == env_var and source["value"] == "#123456"
for source in body["env_sources"]
)
@pytest.mark.usefixtures("db_session")
def test_update_css_settings_persists_changes(
api_client: TestClient, db_session: Session
) -> None:
payload = {"variables": {"--color-primary": "#112233"}}
response = api_client.put("/api/settings/css", json=payload)
assert response.status_code == 200
body = response.json()
assert body["variables"]["--color-primary"] == "#112233"
persisted = settings_service.get_css_color_settings(db_session)
assert persisted["--color-primary"] == "#112233"
@pytest.mark.usefixtures("db_session")
def test_update_css_settings_invalid_value_returns_422(
api_client: TestClient
) -> None:
response = api_client.put(
"/api/settings/css",
json={"variables": {"--color-primary": "not-a-color"}},
)
assert response.status_code == 422
body = response.json()
assert "color" in body["detail"].lower()

View File

@@ -0,0 +1,137 @@
from types import SimpleNamespace
from typing import Dict
import pytest
from sqlalchemy.orm import Session
from models.application_setting import ApplicationSetting
from services import settings as settings_service
from services.settings import CSS_COLOR_DEFAULTS
@pytest.fixture(name="clean_env")
def fixture_clean_env(monkeypatch: pytest.MonkeyPatch) -> Dict[str, str]:
"""Provide an isolated environment mapping for tests."""
env: Dict[str, str] = {}
monkeypatch.setattr(settings_service, "os", SimpleNamespace(environ=env))
return env
def test_css_key_to_env_var_formatting():
assert settings_service.css_key_to_env_var("--color-background") == "CALMINER_THEME_COLOR_BACKGROUND"
assert settings_service.css_key_to_env_var("--color-primary-stronger") == "CALMINER_THEME_COLOR_PRIMARY_STRONGER"
@pytest.mark.parametrize(
"env_key,env_value",
[
("--color-background", "#ffffff"),
("--color-primary", "rgb(10, 20, 30)"),
("--color-accent", "rgba(1,2,3,0.5)"),
("--color-text-secondary", "hsla(210, 40%, 40%, 1)"),
],
)
def test_read_css_color_env_overrides_valid_values(clean_env, env_key, env_value):
env_var = settings_service.css_key_to_env_var(env_key)
clean_env[env_var] = env_value
overrides = settings_service.read_css_color_env_overrides(clean_env)
assert overrides[env_key] == env_value
@pytest.mark.parametrize(
"invalid_value",
[
"", # empty
"not-a-color", # arbitrary string
"#12", # short hex
"rgb(1,2)", # malformed rgb
],
)
def test_read_css_color_env_overrides_invalid_values_raise(clean_env, invalid_value):
env_var = settings_service.css_key_to_env_var("--color-background")
clean_env[env_var] = invalid_value
with pytest.raises(ValueError):
settings_service.read_css_color_env_overrides(clean_env)
def test_read_css_color_env_overrides_ignores_missing(clean_env):
overrides = settings_service.read_css_color_env_overrides(clean_env)
assert overrides == {}
def test_list_css_env_override_rows_returns_structured_data(clean_env):
clean_env[settings_service.css_key_to_env_var("--color-primary")] = "#123456"
rows = settings_service.list_css_env_override_rows(clean_env)
assert rows == [
{
"css_key": "--color-primary",
"env_var": settings_service.css_key_to_env_var("--color-primary"),
"value": "#123456",
}
]
def test_normalize_color_value_strips_and_validates():
assert settings_service._normalize_color_value(" #abcdef ") == "#abcdef"
with pytest.raises(ValueError):
settings_service._normalize_color_value(123) # type: ignore[arg-type]
with pytest.raises(ValueError):
settings_service._normalize_color_value(" ")
with pytest.raises(ValueError):
settings_service._normalize_color_value("#12")
def test_ensure_css_color_settings_creates_defaults(db_session: Session):
settings_service.ensure_css_color_settings(db_session)
stored = {
record.key: record.value
for record in db_session.query(ApplicationSetting).all()
}
assert set(stored.keys()) == set(CSS_COLOR_DEFAULTS.keys())
assert stored == CSS_COLOR_DEFAULTS
def test_update_css_color_settings_persists_changes(db_session: Session):
settings_service.ensure_css_color_settings(db_session)
updated = settings_service.update_css_color_settings(
db_session,
{"--color-background": "#000000", "--color-accent": "#abcdef"},
)
assert updated["--color-background"] == "#000000"
assert updated["--color-accent"] == "#abcdef"
stored = {
record.key: record.value
for record in db_session.query(ApplicationSetting).all()
}
assert stored["--color-background"] == "#000000"
assert stored["--color-accent"] == "#abcdef"
def test_get_css_color_settings_respects_env_overrides(
db_session: Session, clean_env: Dict[str, str]
):
settings_service.ensure_css_color_settings(db_session)
override_value = "#112233"
clean_env[settings_service.css_key_to_env_var("--color-background")] = (
override_value
)
values = settings_service.get_css_color_settings(db_session)
assert values["--color-background"] == override_value
db_value = (
db_session.query(ApplicationSetting)
.filter_by(key="--color-background")
.one()
.value
)
assert db_value != override_value

View File

@@ -4,6 +4,7 @@ import pytest
from fastapi.testclient import TestClient
from models.scenario import Scenario
from services import settings as settings_service
def test_dashboard_route_provides_summary(
@@ -129,3 +130,36 @@ def test_additional_ui_routes_render_templates(
context = cast(Dict[str, Any], getattr(response, "context", {}))
assert context
def test_settings_route_provides_css_context(
api_client: TestClient,
monkeypatch: pytest.MonkeyPatch,
) -> None:
env_var = settings_service.css_key_to_env_var("--color-accent")
monkeypatch.setenv(env_var, "#abcdef")
response = api_client.get("/ui/settings")
assert response.status_code == 200
template = getattr(response, "template", None)
assert template is not None
assert template.name == "settings.html"
context = cast(Dict[str, Any], getattr(response, "context", {}))
assert "css_variables" in context
assert "css_defaults" in context
assert "css_env_overrides" in context
assert "css_env_override_rows" in context
assert "css_env_override_meta" in context
assert context["css_variables"]["--color-accent"] == "#abcdef"
assert context["css_defaults"]["--color-accent"] == settings_service.CSS_COLOR_DEFAULTS["--color-accent"]
assert context["css_env_overrides"]["--color-accent"] == "#abcdef"
override_rows = context["css_env_override_rows"]
assert any(row["env_var"] == env_var for row in override_rows)
meta = context["css_env_override_meta"]["--color-accent"]
assert meta["value"] == "#abcdef"
assert meta["env_var"] == env_var