feat: closed-set admin knobs are pickers, not free text

log_level, search_source, download_layout and the transcode hw/preset
were typed by hand, and every consumer carried its own guess at what a
typo meant. The value set is now a Literal in config.py: pydantic
rejects a bad env value at boot (any letter case accepted), sysconfig
refuses a bad override and derives the admin picker's options from the
same Literal, and the consumers read the value as-is.

The Configuration page renders a choice key as a segmented bar, or a
dropdown past six options, with translated option names. Its tabs now
follow the registry's order instead of a hand-kept copy that had
already dropped the explore group.

A single-choice FilterSelect now opens on the option in use: in the
browser it could land on the first row instead.
This commit is contained in:
2026-09-17 00:03:56 +02:00
parent 6c63df6704
commit 07766c43e8
15 changed files with 496 additions and 93 deletions
+13 -13
View File
@@ -295,20 +295,20 @@ _THIRD_PARTY_LEVELS = {
}
def resolve_level(raw: str | None) -> int:
"""Level name (case-insensitive) → number, falling back to INFO for an unknown value. A typo in
a config knob must not silence the app."""
if not raw:
return logging.INFO
value = logging.getLevelName(raw.strip().upper())
return value if isinstance(value, int) else logging.INFO
def resolve_level(name: str) -> int:
"""Level name → number. Both ways in (the `LOG_LEVEL` env and the `log_level` admin knob) are
already held to the `LogLevel` choices, so an unknown name here is a programming error."""
value = logging.getLevelName(name)
if not isinstance(value, int):
raise ValueError(f"unknown log level {name!r}")
return value
def configure_logging(source: str, *, level: str | None = None) -> None:
def configure_logging(source: str) -> None:
"""Configure logging for THIS process. Call once, first thing, in every entry point:
`main.py` (api), `worker.main`, `downloads/runner.main`, `alembic/env.py`.
`source` names the process in every line. `level` overrides `LOG_LEVEL` (the bootstrap default);
`source` names the process in every line. The level starts at `LOG_LEVEL` (the bootstrap default);
the runtime admin knob goes through `set_level()` instead, so changing it needs no restart.
Our loggers deliberately keep `propagate=True` and own no handler: everything reaches the single
@@ -316,7 +316,7 @@ def configure_logging(source: str, *, level: str | None = None) -> None:
is configured the moment it is created."""
global _source
_source = source
formatter = "json" if (settings.log_format or "json").lower() != "text" else "text"
formatter = settings.log_format
logging.config.dictConfig(
{
"version": 1,
@@ -336,16 +336,16 @@ def configure_logging(source: str, *, level: str | None = None) -> None:
"filters": ["context"],
}
},
"root": {"level": resolve_level(level or settings.log_level), "handlers": ["stdout"]},
"root": {"level": resolve_level(settings.log_level), "handlers": ["stdout"]},
"loggers": {name: {"level": lvl} for name, lvl in _THIRD_PARTY_LEVELS.items()},
}
)
def set_level(raw: str | None) -> None:
def set_level(name: str) -> None:
"""Change the running process's level (the `log_level` admin knob). Third-party loggers keep
their pinned levels, so turning on DEBUG shows OUR detail, not SQLAlchemy's."""
logging.getLogger().setLevel(resolve_level(raw))
logging.getLogger().setLevel(resolve_level(name))
def current_level() -> str: