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:
+13
-13
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user