generated from john/python-template
@@ -14,15 +14,20 @@ Settings manages installation-local registries, safe runtime .env settings, and
|
|||||||
|
|
||||||
- The page title is **Settings**.
|
- The page title is **Settings**.
|
||||||
- Configuration surfaces are grouped as tabs:
|
- Configuration surfaces are grouped as tabs:
|
||||||
- **Runtime Settings**
|
|
||||||
- **Document Types**
|
- **Document Types**
|
||||||
- **Person Roles**
|
- **Person Roles**
|
||||||
- **Tags**
|
- **Tags**
|
||||||
- **Prompts**
|
- **Prompts**
|
||||||
- **Home Page Text**
|
- **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 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 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 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.
|
- 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.
|
- Prompts exposes only `transcribe_document.md` for editing and restore-from-backup.
|
||||||
- Home Page Text edits the same Markdown content rendered on `/homepage`.
|
- 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.components.table.registry import render_registry_table
|
||||||
from transcription.ui.homepage_store import read_homepage_markdown
|
from transcription.ui.homepage_store import read_homepage_markdown
|
||||||
from transcription.ui.homepage_store import save_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 read_runtime_settings_snapshot
|
||||||
from transcription.ui.runtime_settings_store import save_runtime_settings
|
from transcription.ui.runtime_settings_store import save_runtime_settings
|
||||||
from transcription.ui.theme import page_header
|
from transcription.ui.theme import page_header
|
||||||
@@ -502,12 +503,10 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
|
|||||||
@ui.refreshable
|
@ui.refreshable
|
||||||
async def render_runtime_settings() -> None:
|
async def render_runtime_settings() -> None:
|
||||||
with archival_card("Runtime Settings"):
|
with archival_card("Runtime Settings"):
|
||||||
ui.label("Edit non-secret .env settings. Secret fields are intentionally excluded.").classes(
|
ui.label(
|
||||||
"text-xs ui-text-muted"
|
"Edit non-secret .env settings. Secret fields are intentionally excluded. "
|
||||||
)
|
"Changes are persisted to .env and apply after restart."
|
||||||
ui.label("Changes are persisted to .env and apply after restart.").classes(
|
).classes("text-xs ui-text-muted mb-3")
|
||||||
"text-xs ui-text-muted mb-3"
|
|
||||||
)
|
|
||||||
snapshot_outcome = await run_ui_action(
|
snapshot_outcome = await run_ui_action(
|
||||||
operation="settings.runtime.read",
|
operation="settings.runtime.read",
|
||||||
title="Runtime settings unavailable",
|
title="Runtime settings unavailable",
|
||||||
@@ -517,27 +516,55 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
|
|||||||
return
|
return
|
||||||
snapshot = snapshot_outcome.value
|
snapshot = snapshot_outcome.value
|
||||||
field_controls: dict[str, Any] = {}
|
field_controls: dict[str, Any] = {}
|
||||||
for field in snapshot.fields:
|
with ui.column().classes("w-full max-w-3xl mx-auto gap-0.5"):
|
||||||
with ui.column().classes("w-full gap-1 py-2 ui-header-divider"):
|
with ui.row().classes("w-full items-center px-2 py-1 border-b ui-border-subtle"):
|
||||||
ui.label(field.label).classes("text-sm font-semibold")
|
ui.label("Setting").classes("w-56 text-xs font-semibold ui-text-muted")
|
||||||
ui.label(field.description).classes("text-xs ui-text-muted")
|
ui.label("Value").classes("text-xs font-semibold ui-text-muted")
|
||||||
if field.control == "bool":
|
|
||||||
control = ui.checkbox(field.env_key, value=bool(field.value))
|
for field in snapshot.fields:
|
||||||
elif field.control == "select":
|
with ui.row().classes("w-full items-start gap-3 px-2 py-1 border-b ui-border-subtle"):
|
||||||
control = (
|
with ui.column().classes("w-56 gap-0"):
|
||||||
ui.select(
|
ui.label(field.label).classes("text-xs font-semibold")
|
||||||
list(field.options),
|
ui.label(f"{field.env_key} · {field.description}").classes(
|
||||||
label=field.env_key,
|
"text-[11px] ui-text-muted"
|
||||||
value=str(field.value),
|
|
||||||
)
|
)
|
||||||
.props("outlined")
|
with ui.column().classes("flex-1 min-w-0"):
|
||||||
.classes("w-full")
|
if field.control == "bool":
|
||||||
)
|
control = ui.checkbox("", value=bool(field.value)).props("dense")
|
||||||
else:
|
elif field.control == "select":
|
||||||
control = (
|
control = (
|
||||||
ui.input(field.env_key, value=str(field.value)).props("outlined").classes("w-full")
|
ui.select(
|
||||||
)
|
list(field.options),
|
||||||
field_controls[field.field_name] = control
|
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:
|
async def save_runtime() -> None:
|
||||||
updates: dict[str, str | bool] = {}
|
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")
|
ui.button("Save runtime settings", icon="save", on_click=save_runtime).classes("ui-btn-primary")
|
||||||
|
|
||||||
with ui.tabs().classes("w-full") as tabs:
|
with ui.tabs().classes("w-full") as tabs:
|
||||||
runtime_settings_tab = ui.tab("Runtime Settings")
|
|
||||||
document_types_tab = ui.tab("Document Types")
|
document_types_tab = ui.tab("Document Types")
|
||||||
person_roles_tab = ui.tab("Person Roles")
|
person_roles_tab = ui.tab("Person Roles")
|
||||||
tags_tab = ui.tab("Tags")
|
tags_tab = ui.tab("Tags")
|
||||||
prompts_tab = ui.tab("Prompts")
|
prompts_tab = ui.tab("Prompts")
|
||||||
home_page_text_tab = ui.tab("Home Page Text")
|
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_panels(tabs, value=document_types_tab).classes("w-full"):
|
||||||
with ui.tab_panel(runtime_settings_tab):
|
|
||||||
await render_runtime_settings()
|
|
||||||
with ui.tab_panel(document_types_tab):
|
with ui.tab_panel(document_types_tab):
|
||||||
await render_document_types()
|
await render_document_types()
|
||||||
with ui.tab_panel(person_roles_tab):
|
with ui.tab_panel(person_roles_tab):
|
||||||
@@ -581,6 +606,8 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
|
|||||||
await render_prompts()
|
await render_prompts()
|
||||||
with ui.tab_panel(home_page_text_tab):
|
with ui.tab_panel(home_page_text_tab):
|
||||||
await render_home_page_text()
|
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:
|
def _selected_table_row(table: Any) -> dict[str, Any] | None:
|
||||||
|
|||||||
@@ -49,6 +49,15 @@ class RuntimeSettingDescriptor:
|
|||||||
options: tuple[str, ...] = ()
|
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(
|
SETTINGS_UI_EXCLUDED_FIELDS = frozenset(
|
||||||
{
|
{
|
||||||
"openrouter_api_key",
|
"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, ...] = (
|
RUNTIME_SETTINGS_CATALOG: tuple[RuntimeSettingDescriptor, ...] = (
|
||||||
RuntimeSettingDescriptor("host", "HOST", "Host", "Server bind host.", "text"),
|
RuntimeSettingDescriptor("host", "HOST", "Host", "Server bind host.", "text"),
|
||||||
RuntimeSettingDescriptor("port", "PORT", "Port", "Server bind port.", "text"),
|
RuntimeSettingDescriptor("port", "PORT", "Port", "Server bind port.", "text"),
|
||||||
|
|||||||
@@ -32,4 +32,5 @@ class TestPageRegistration:
|
|||||||
assert "Tags" in settings_response.text
|
assert "Tags" in settings_response.text
|
||||||
assert "Prompts" in settings_response.text
|
assert "Prompts" in settings_response.text
|
||||||
assert "Home Page Text" 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
|
assert "README.md" not in settings_response.text
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import pytest
|
|||||||
|
|
||||||
from transcription.config import Settings
|
from transcription.config import Settings
|
||||||
from transcription.errors import AppError
|
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 RUNTIME_SETTINGS_CATALOG
|
||||||
from transcription.ui.runtime_settings_store import SETTINGS_UI_EXCLUDED_FIELDS
|
from transcription.ui.runtime_settings_store import SETTINGS_UI_EXCLUDED_FIELDS
|
||||||
from transcription.ui.runtime_settings_store import read_runtime_settings_snapshot
|
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()
|
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):
|
def test_runtime_settings_snapshot_includes_catalog_fields(tmp_path: Path):
|
||||||
settings = _settings_for_runtime_editing(tmp_path)
|
settings = _settings_for_runtime_editing(tmp_path)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user