From c05c054b947db5ad8aa2d40aa5976d6e35507a2a Mon Sep 17 00:00:00 2001 From: Jim Lancaster <40281233+zoltan57@users.noreply.github.com> Date: Mon, 24 Aug 2026 17:08:09 -0500 Subject: [PATCH] Refine Settings page --- docs/ui/pages/settings.md | 7 +- src/transcription/ui/pages/settings_page.py | 87 ++++++++++++------- .../ui/runtime_settings_store.py | 29 +++++++ tests/ui/test_pages_registration.py | 1 + tests/ui/test_runtime_settings_store.py | 9 ++ 5 files changed, 102 insertions(+), 31 deletions(-) diff --git a/docs/ui/pages/settings.md b/docs/ui/pages/settings.md index 0048711..4d1fea9 100644 --- a/docs/ui/pages/settings.md +++ b/docs/ui/pages/settings.md @@ -14,15 +14,20 @@ Settings manages installation-local registries, safe runtime .env settings, and - The page title is **Settings**. - Configuration surfaces are grouped as tabs: - - **Runtime Settings** - **Document Types** - **Person Roles** - **Tags** - **Prompts** - **Home Page Text** + - **Runtime Settings** - Runtime Settings exposes an allowlisted set of non-secret fields synchronized with `Settings` model fields except excluded secret/unsafe fields. +- Runtime Settings is rendered as a compact two-column editor (**Setting**, **Value**) in a centered, narrower responsive container. - Runtime Settings persists changes to `.env`, validates by constructing a `Settings` instance, and reports validation failures through the shared UI error presenter. - Runtime Settings changes require application restart to take effect. +- Runtime Settings includes an explicit "Other settings not shown here" markdown table listing: + - secrets (`OPENROUTER_API_KEY`, `DATABASE__PASSWORD`) + - high-risk database connection settings (`DATABASE__DRIVER`, `DATABASE__PATH`, `DATABASE__HOST`, `DATABASE__PORT`, `DATABASE__DATABASE`, `DATABASE__USER`) + and directs edits for those keys to `.env`. - Document Types, Person Roles, and Tags support Add/Edit/Delete with existing guardrails. - Prompts exposes only `transcribe_document.md` for editing and restore-from-backup. - Home Page Text edits the same Markdown content rendered on `/homepage`. diff --git a/src/transcription/ui/pages/settings_page.py b/src/transcription/ui/pages/settings_page.py index b913990..7fa0e15 100644 --- a/src/transcription/ui/pages/settings_page.py +++ b/src/transcription/ui/pages/settings_page.py @@ -21,6 +21,7 @@ from transcription.ui.components.primitives import section_header_row from transcription.ui.components.table.registry import render_registry_table from transcription.ui.homepage_store import read_homepage_markdown from transcription.ui.homepage_store import save_homepage_markdown +from transcription.ui.runtime_settings_store import HIDDEN_SETTINGS_CATEGORIES from transcription.ui.runtime_settings_store import read_runtime_settings_snapshot from transcription.ui.runtime_settings_store import save_runtime_settings from transcription.ui.theme import page_header @@ -502,12 +503,10 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915 @ui.refreshable async def render_runtime_settings() -> None: with archival_card("Runtime Settings"): - ui.label("Edit non-secret .env settings. Secret fields are intentionally excluded.").classes( - "text-xs ui-text-muted" - ) - ui.label("Changes are persisted to .env and apply after restart.").classes( - "text-xs ui-text-muted mb-3" - ) + ui.label( + "Edit non-secret .env settings. Secret fields are intentionally excluded. " + "Changes are persisted to .env and apply after restart." + ).classes("text-xs ui-text-muted mb-3") snapshot_outcome = await run_ui_action( operation="settings.runtime.read", title="Runtime settings unavailable", @@ -517,27 +516,55 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915 return snapshot = snapshot_outcome.value field_controls: dict[str, Any] = {} - for field in snapshot.fields: - with ui.column().classes("w-full gap-1 py-2 ui-header-divider"): - ui.label(field.label).classes("text-sm font-semibold") - ui.label(field.description).classes("text-xs ui-text-muted") - if field.control == "bool": - control = ui.checkbox(field.env_key, value=bool(field.value)) - elif field.control == "select": - control = ( - ui.select( - list(field.options), - label=field.env_key, - value=str(field.value), + with ui.column().classes("w-full max-w-3xl mx-auto gap-0.5"): + with ui.row().classes("w-full items-center px-2 py-1 border-b ui-border-subtle"): + ui.label("Setting").classes("w-56 text-xs font-semibold ui-text-muted") + ui.label("Value").classes("text-xs font-semibold ui-text-muted") + + for field in snapshot.fields: + with ui.row().classes("w-full items-start gap-3 px-2 py-1 border-b ui-border-subtle"): + with ui.column().classes("w-56 gap-0"): + ui.label(field.label).classes("text-xs font-semibold") + ui.label(f"{field.env_key} ยท {field.description}").classes( + "text-[11px] ui-text-muted" ) - .props("outlined") - .classes("w-full") - ) - else: - control = ( - ui.input(field.env_key, value=str(field.value)).props("outlined").classes("w-full") - ) - field_controls[field.field_name] = control + with ui.column().classes("flex-1 min-w-0"): + if field.control == "bool": + control = ui.checkbox("", value=bool(field.value)).props("dense") + elif field.control == "select": + control = ( + ui.select( + list(field.options), + label="", + value=str(field.value), + ) + .props("outlined dense") + .classes("w-full") + ) + else: + control = ( + ui.input("", value=str(field.value)) + .props("outlined dense") + .classes("w-full") + ) + field_controls[field.field_name] = control + + with ui.column().classes("w-full max-w-3xl mx-auto gap-2 mt-3"): + ui.label("Other settings not shown here").classes("text-sm font-semibold") + ui.label("Edit these directly in .env:").classes("text-xs ui-text-muted") + table_rows = [ + f"| {category.title} | {', '.join(f'`{key}`' for key in category.env_keys)} |" + for category in HIDDEN_SETTINGS_CATEGORIES + ] + ui.markdown( + "\n".join( + [ + "| **Category** | **Settings** |", + "|---|---|", + *table_rows, + ] + ) + ).classes("w-full text-xs") async def save_runtime() -> None: updates: dict[str, str | bool] = {} @@ -561,16 +588,14 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915 ui.button("Save runtime settings", icon="save", on_click=save_runtime).classes("ui-btn-primary") with ui.tabs().classes("w-full") as tabs: - runtime_settings_tab = ui.tab("Runtime Settings") document_types_tab = ui.tab("Document Types") person_roles_tab = ui.tab("Person Roles") tags_tab = ui.tab("Tags") prompts_tab = ui.tab("Prompts") home_page_text_tab = ui.tab("Home Page Text") + runtime_settings_tab = ui.tab("Runtime Settings") - with ui.tab_panels(tabs, value=runtime_settings_tab).classes("w-full"): - with ui.tab_panel(runtime_settings_tab): - await render_runtime_settings() + with ui.tab_panels(tabs, value=document_types_tab).classes("w-full"): with ui.tab_panel(document_types_tab): await render_document_types() with ui.tab_panel(person_roles_tab): @@ -581,6 +606,8 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915 await render_prompts() with ui.tab_panel(home_page_text_tab): await render_home_page_text() + with ui.tab_panel(runtime_settings_tab): + await render_runtime_settings() def _selected_table_row(table: Any) -> dict[str, Any] | None: diff --git a/src/transcription/ui/runtime_settings_store.py b/src/transcription/ui/runtime_settings_store.py index 80d5e85..1983977 100644 --- a/src/transcription/ui/runtime_settings_store.py +++ b/src/transcription/ui/runtime_settings_store.py @@ -49,6 +49,15 @@ class RuntimeSettingDescriptor: options: tuple[str, ...] = () +@dataclass(frozen=True, slots=True) +class HiddenSettingsCategory: + """Settings excluded from the UI editor and why they are excluded.""" + + title: str + reason: str + env_keys: tuple[str, ...] + + SETTINGS_UI_EXCLUDED_FIELDS = frozenset( { "openrouter_api_key", @@ -56,6 +65,26 @@ SETTINGS_UI_EXCLUDED_FIELDS = frozenset( } ) +HIDDEN_SETTINGS_CATEGORIES: tuple[HiddenSettingsCategory, ...] = ( + HiddenSettingsCategory( + title="Not safe to expose/edit as plain text in UI (secrets)", + reason="These values are credentials and must remain hidden from page rendering and client responses.", + env_keys=("OPENROUTER_API_KEY", "DATABASE__PASSWORD"), + ), + HiddenSettingsCategory( + title="Non-secret but high-risk (can break runtime/connectivity; still editable with guardrails)", + reason="These values control database connectivity and can make the app unavailable if changed incorrectly.", + env_keys=( + "DATABASE__DRIVER", + "DATABASE__PATH", + "DATABASE__HOST", + "DATABASE__PORT", + "DATABASE__DATABASE", + "DATABASE__USER", + ), + ), +) + RUNTIME_SETTINGS_CATALOG: tuple[RuntimeSettingDescriptor, ...] = ( RuntimeSettingDescriptor("host", "HOST", "Host", "Server bind host.", "text"), RuntimeSettingDescriptor("port", "PORT", "Port", "Server bind port.", "text"), diff --git a/tests/ui/test_pages_registration.py b/tests/ui/test_pages_registration.py index 1950803..1a78382 100644 --- a/tests/ui/test_pages_registration.py +++ b/tests/ui/test_pages_registration.py @@ -32,4 +32,5 @@ class TestPageRegistration: assert "Tags" in settings_response.text assert "Prompts" in settings_response.text assert "Home Page Text" in settings_response.text + assert "Other settings not shown here" in settings_response.text assert "README.md" not in settings_response.text diff --git a/tests/ui/test_runtime_settings_store.py b/tests/ui/test_runtime_settings_store.py index c262797..f2fbdeb 100644 --- a/tests/ui/test_runtime_settings_store.py +++ b/tests/ui/test_runtime_settings_store.py @@ -8,6 +8,7 @@ import pytest from transcription.config import Settings from transcription.errors import AppError +from transcription.ui.runtime_settings_store import HIDDEN_SETTINGS_CATEGORIES from transcription.ui.runtime_settings_store import RUNTIME_SETTINGS_CATALOG from transcription.ui.runtime_settings_store import SETTINGS_UI_EXCLUDED_FIELDS from transcription.ui.runtime_settings_store import read_runtime_settings_snapshot @@ -28,6 +29,14 @@ def test_runtime_settings_catalog_classifies_all_non_secret_settings_fields(): assert "openrouter_api_key" not in settings_catalog_field_names() +def test_hidden_settings_categories_list_secret_and_high_risk_env_keys(): + hidden_keys = {key for category in HIDDEN_SETTINGS_CATEGORIES for key in category.env_keys} + assert "OPENROUTER_API_KEY" in hidden_keys + assert "DATABASE__PASSWORD" in hidden_keys + assert "DATABASE__DRIVER" in hidden_keys + assert "DATABASE__HOST" in hidden_keys + + def test_runtime_settings_snapshot_includes_catalog_fields(tmp_path: Path): settings = _settings_for_runtime_editing(tmp_path)