feat: editable branding — app name and subtitle configurable in Admin Settings
Adds app_name and app_subtitle as system settings with a dedicated Branding tab in the Admin panel. Both values are served via a public GET /api/admin/branding endpoint (no auth) so the login page can also show the configured name before the user signs in. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,8 @@ VALID_ENGINES = {"cycles", "eevee"}
|
||||
VALID_FORMATS = {"jpg", "png"}
|
||||
VALID_CYCLES_DEVICES = {"auto", "gpu", "cpu"}
|
||||
SETTINGS_DEFAULTS: dict[str, str] = {
|
||||
"app_name": "Hart.O.Mat",
|
||||
"app_subtitle": "Hartomatisierung",
|
||||
"thumbnail_renderer": "blender",
|
||||
"blender_engine": "cycles",
|
||||
"blender_cycles_samples": "256",
|
||||
@@ -59,6 +61,8 @@ SETTINGS_DEFAULTS: dict[str, str] = {
|
||||
|
||||
|
||||
class SettingsOut(BaseModel):
|
||||
app_name: str = "Hart.O.Mat"
|
||||
app_subtitle: str = "Hartomatisierung"
|
||||
thumbnail_renderer: str = "blender"
|
||||
blender_engine: str = "cycles"
|
||||
blender_cycles_samples: int = 256
|
||||
@@ -91,6 +95,8 @@ class SettingsOut(BaseModel):
|
||||
|
||||
|
||||
class SettingsUpdate(BaseModel):
|
||||
app_name: str | None = None
|
||||
app_subtitle: str | None = None
|
||||
thumbnail_renderer: str | None = None
|
||||
blender_engine: str | None = None
|
||||
blender_cycles_samples: int | None = None
|
||||
@@ -209,6 +215,8 @@ async def _save_setting(db: AsyncSession, key: str, value: str) -> None:
|
||||
|
||||
def _settings_to_out(raw: dict[str, str]) -> SettingsOut:
|
||||
return SettingsOut(
|
||||
app_name=raw.get("app_name", "Hart.O.Mat"),
|
||||
app_subtitle=raw.get("app_subtitle", "Hartomatisierung"),
|
||||
thumbnail_renderer=raw["thumbnail_renderer"],
|
||||
blender_engine=raw["blender_engine"],
|
||||
blender_cycles_samples=int(raw["blender_cycles_samples"]),
|
||||
@@ -241,6 +249,19 @@ def _settings_to_out(raw: dict[str, str]) -> SettingsOut:
|
||||
)
|
||||
|
||||
|
||||
@router.get("/branding")
|
||||
async def get_branding(db: AsyncSession = Depends(get_db)):
|
||||
"""Public endpoint — returns configured app name and subtitle without authentication."""
|
||||
result = await db.execute(
|
||||
select(SystemSetting).where(SystemSetting.key.in_(["app_name", "app_subtitle"]))
|
||||
)
|
||||
rows = {row.key: row.value for row in result.scalars().all()}
|
||||
return {
|
||||
"app_name": rows.get("app_name", "Hart.O.Mat"),
|
||||
"app_subtitle": rows.get("app_subtitle", "Hartomatisierung"),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/settings", response_model=SettingsOut)
|
||||
async def get_settings(
|
||||
admin: User = Depends(require_global_admin),
|
||||
@@ -255,6 +276,14 @@ async def update_settings(
|
||||
admin: User = Depends(require_global_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
if body.app_name is not None:
|
||||
stripped = body.app_name.strip()
|
||||
if not stripped:
|
||||
raise HTTPException(400, detail="app_name cannot be empty")
|
||||
if len(stripped) > 100:
|
||||
raise HTTPException(400, detail="app_name must be 100 characters or fewer")
|
||||
if body.app_subtitle is not None and len(body.app_subtitle) > 100:
|
||||
raise HTTPException(400, detail="app_subtitle must be 100 characters or fewer")
|
||||
if body.thumbnail_renderer is not None and body.thumbnail_renderer not in VALID_RENDERERS:
|
||||
raise HTTPException(400, detail=f"Invalid renderer. Choose: {', '.join(sorted(VALID_RENDERERS))}")
|
||||
if body.blender_engine is not None and body.blender_engine not in VALID_ENGINES:
|
||||
@@ -292,6 +321,10 @@ async def update_settings(
|
||||
raise HTTPException(400, detail=f"Output type '{entry}' not found")
|
||||
|
||||
updates: dict[str, str] = {}
|
||||
if body.app_name is not None:
|
||||
updates["app_name"] = body.app_name.strip()
|
||||
if body.app_subtitle is not None:
|
||||
updates["app_subtitle"] = body.app_subtitle.strip()
|
||||
if body.thumbnail_renderer is not None:
|
||||
updates["thumbnail_renderer"] = body.thumbnail_renderer
|
||||
if body.blender_engine is not None:
|
||||
|
||||
Reference in New Issue
Block a user