diff --git a/backend/app/routers/admin.py b/backend/app/routers/admin.py
index 794c682..b559daa 100644
--- a/backend/app/routers/admin.py
+++ b/backend/app/routers/admin.py
@@ -122,3 +122,35 @@ async def manual_purge(
db = request.app.state.db
result = await run_purge(db, config)
return result
+
+
+@router.put("/settings")
+async def update_settings(
+ request: Request,
+ _admin: str = Depends(require_admin),
+) -> dict:
+ """Update runtime settings (in-memory only — resets on restart).
+
+ Accepts a JSON body with optional fields:
+ - welcome_message: str
+ """
+ body = await request.json()
+
+ if not hasattr(request.app.state, "settings_overrides"):
+ request.app.state.settings_overrides = {}
+
+ updated = []
+ if "welcome_message" in body:
+ msg = body["welcome_message"]
+ if not isinstance(msg, str):
+ from fastapi.responses import JSONResponse
+
+ return JSONResponse(
+ status_code=422,
+ content={"detail": "welcome_message must be a string"},
+ )
+ request.app.state.settings_overrides["welcome_message"] = msg
+ updated.append("welcome_message")
+ logger.info("Admin updated welcome_message to: %s", msg[:80])
+
+ return {"updated": updated, "status": "ok"}
diff --git a/backend/app/routers/system.py b/backend/app/routers/system.py
index 8aa6004..d2db471 100644
--- a/backend/app/routers/system.py
+++ b/backend/app/routers/system.py
@@ -20,10 +20,16 @@ async def public_config(request: Request) -> dict:
is fragile when new sensitive fields are added later.
"""
config = request.app.state.config
+
+ # Runtime overrides (set via admin settings endpoint) take precedence
+ overrides = getattr(request.app.state, "settings_overrides", {})
+
return {
"session_mode": config.session.mode,
"default_theme": config.ui.default_theme,
- "welcome_message": config.ui.welcome_message,
+ "welcome_message": overrides.get(
+ "welcome_message", config.ui.welcome_message
+ ),
"purge_enabled": config.purge.enabled,
"max_concurrent_downloads": config.downloads.max_concurrent,
}
diff --git a/frontend/src/components/AdminPanel.vue b/frontend/src/components/AdminPanel.vue
index b60cae9..5e9c9c1 100644
--- a/frontend/src/components/AdminPanel.vue
+++ b/frontend/src/components/AdminPanel.vue
@@ -1,10 +1,15 @@
@@ -32,7 +55,7 @@ async function switchTab(tab: typeof activeTab.value) {
+
+
+
+
+
+
Displayed above the URL input on the main page. Leave empty to hide.
+
+
+
+
+ ✓ Saved
+
+
+ Changes are applied immediately but reset on server restart.
+
+
@@ -239,4 +290,62 @@ h3 {
color: var(--color-text-muted);
text-align: center;
}
+
+.settings-field {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-xs);
+}
+
+.settings-field label {
+ font-weight: 600;
+ color: var(--color-text);
+}
+
+.field-hint {
+ font-size: var(--font-size-sm);
+ color: var(--color-text-muted);
+ margin: 0;
+}
+
+.settings-textarea {
+ width: 100%;
+ padding: var(--space-sm);
+ background: var(--color-bg);
+ color: var(--color-text);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-sm);
+ font-family: var(--font-ui);
+ font-size: var(--font-size-base);
+ resize: vertical;
+}
+
+.settings-textarea:focus {
+ outline: none;
+ border-color: var(--color-accent);
+}
+
+.settings-actions {
+ display: flex;
+ align-items: center;
+ gap: var(--space-md);
+ margin-top: var(--space-md);
+}
+
+.btn-save {
+ background: var(--color-accent);
+ color: var(--color-bg);
+ font-weight: 600;
+ padding: var(--space-sm) var(--space-lg);
+}
+
+.btn-save:hover:not(:disabled) {
+ background: var(--color-accent-hover);
+}
+
+.save-confirm {
+ color: var(--color-success);
+ font-weight: 500;
+ font-size: var(--font-size-sm);
+}
diff --git a/frontend/src/components/DownloadItem.vue b/frontend/src/components/DownloadItem.vue
deleted file mode 100644
index 3ff9c0b..0000000
--- a/frontend/src/components/DownloadItem.vue
+++ /dev/null
@@ -1,179 +0,0 @@
-
-
-
-
-
-
-
-
-
- {{ job.speed }}
- ETA: {{ job.eta }}
- {{ job.error_message }}
-
-
-
-
-
-
-
-
-
diff --git a/frontend/src/components/ThemePicker.vue b/frontend/src/components/ThemePicker.vue
deleted file mode 100644
index 64778af..0000000
--- a/frontend/src/components/ThemePicker.vue
+++ /dev/null
@@ -1,105 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/frontend/src/stores/admin.ts b/frontend/src/stores/admin.ts
index 7ddbb30..d62cc75 100644
--- a/frontend/src/stores/admin.ts
+++ b/frontend/src/stores/admin.ts
@@ -110,6 +110,23 @@ export const useAdminStore = defineStore('admin', () => {
}
}
+ async function updateSettings(data: Record): Promise {
+ isLoading.value = true
+ try {
+ const res = await fetch('/api/admin/settings', {
+ method: 'PUT',
+ headers: {
+ ..._authHeaders(),
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(data),
+ })
+ return res.ok
+ } finally {
+ isLoading.value = false
+ }
+ }
+
return {
username,
isAuthenticated,
@@ -123,5 +140,6 @@ export const useAdminStore = defineStore('admin', () => {
loadSessions,
loadStorage,
triggerPurge,
+ updateSettings,
}
})