generated from john/python-template
@@ -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`.
|
||||
|
||||
@@ -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,28 +516,56 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
|
||||
return
|
||||
snapshot = snapshot_outcome.value
|
||||
field_controls: dict[str, Any] = {}
|
||||
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.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")
|
||||
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"
|
||||
)
|
||||
with ui.column().classes("flex-1 min-w-0"):
|
||||
if field.control == "bool":
|
||||
control = ui.checkbox(field.env_key, value=bool(field.value))
|
||||
control = ui.checkbox("", value=bool(field.value)).props("dense")
|
||||
elif field.control == "select":
|
||||
control = (
|
||||
ui.select(
|
||||
list(field.options),
|
||||
label=field.env_key,
|
||||
label="",
|
||||
value=str(field.value),
|
||||
)
|
||||
.props("outlined")
|
||||
.props("outlined dense")
|
||||
.classes("w-full")
|
||||
)
|
||||
else:
|
||||
control = (
|
||||
ui.input(field.env_key, value=str(field.value)).props("outlined").classes("w-full")
|
||||
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] = {}
|
||||
for field in snapshot.fields:
|
||||
@@ -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:
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user