From 9b62d5046129d30486a868cd96b148ea4cbb3ffe Mon Sep 17 00:00:00 2001 From: xpltd Date: Wed, 18 Mar 2026 21:34:46 -0500 Subject: [PATCH] =?UTF-8?q?GSD:=20M002/S03=20complete=20=E2=80=94=20Mobile?= =?UTF-8?q?=20+=20integration=20polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Admin panel: Settings tab with welcome message editor (runtime override) - Backend: PUT /api/admin/settings endpoint for runtime config - Backend: public config reads runtime overrides (settings_overrides on app.state) - Removed unused ThemePicker.vue (replaced by DarkModeToggle in S01) - Removed unused DownloadItem.vue (replaced by DownloadTable in S02) - All 34 frontend + 179 backend tests passing - M002 COMPLETE — all 3 slices done --- .gsd/milestones/M002/M002-ROADMAP.md | 2 +- .gsd/milestones/M002/slices/S03/S03-PLAN.md | 52 ++++++ backend/app/routers/admin.py | 32 ++++ backend/app/routers/system.py | 8 +- frontend/src/components/AdminPanel.vue | 113 +++++++++++- frontend/src/components/DownloadItem.vue | 179 -------------------- frontend/src/components/ThemePicker.vue | 105 ------------ frontend/src/stores/admin.ts | 18 ++ 8 files changed, 221 insertions(+), 288 deletions(-) create mode 100644 .gsd/milestones/M002/slices/S03/S03-PLAN.md delete mode 100644 frontend/src/components/DownloadItem.vue delete mode 100644 frontend/src/components/ThemePicker.vue diff --git a/.gsd/milestones/M002/M002-ROADMAP.md b/.gsd/milestones/M002/M002-ROADMAP.md index a5a46b3..09480ce 100644 --- a/.gsd/milestones/M002/M002-ROADMAP.md +++ b/.gsd/milestones/M002/M002-ROADMAP.md @@ -59,7 +59,7 @@ This milestone is complete only when all are true: - [x] **S02: Download Flow + Queue Redesign** `risk:medium` `depends:[S01]` > After this: Single "Download" button with optional format picker, audio/video toggle, queue displays as styled table with sorting, completed items show download/copy/clear glyphs -- [ ] **S03: Mobile + Integration Polish** `risk:low` `depends:[S02]` +- [x] **S03: Mobile + Integration Polish** `risk:low` `depends:[S02]` > After this: Mobile layout works with new table design, admin welcome message editor functional, all flows verified end-to-end ## Boundary Map diff --git a/.gsd/milestones/M002/slices/S03/S03-PLAN.md b/.gsd/milestones/M002/slices/S03/S03-PLAN.md new file mode 100644 index 0000000..27837d3 --- /dev/null +++ b/.gsd/milestones/M002/slices/S03/S03-PLAN.md @@ -0,0 +1,52 @@ +# S03: Mobile + Integration Polish + +**Goal:** Ensure mobile view works cleanly with the new table-based queue, add a welcome message editor in the admin panel, and verify all flows end-to-end. +**Demo:** Mobile user can submit downloads and view the queue table. Admin can edit the welcome message text from the admin panel Settings tab. All navigation flows work. + +## Must-Haves + +- Mobile queue table is usable (tested at 390px viewport) +- Admin panel has a "Settings" tab with welcome message text editor +- Admin settings tab saves welcome_message via backend API +- All end-to-end flows verified in browser (desktop + mobile) +- No test regressions + +## Verification + +- `cd frontend && npx vitest run` — all tests pass +- `cd backend && source .venv/Scripts/activate && python -m pytest tests/ -q -m "not integration"` — no regressions +- Browser (desktop): full download lifecycle works +- Browser (mobile 390px): submit + queue table renders, actions work +- Browser: admin panel Settings tab → edit welcome message → saves + +## Tasks + +- [x] **T01: Admin welcome message editor** `est:30m` + - Why: Operators need to customize the welcome message without editing config files + - Files: `frontend/src/components/AdminPanel.vue`, `frontend/src/stores/admin.ts`, `backend/app/routers/admin.py` + - Do: Add a "Settings" tab to admin panel with a textarea for welcome message. Load current value from `/api/config/public`. Add PUT `/api/admin/settings` endpoint that updates the config's welcome_message in memory (runtime override — persisting to YAML is out of scope for this milestone). Add `updateSettings(data)` to admin store. Show save confirmation. + - Verify: Login to admin → Settings tab → edit message → save → reload main page → new message visible + - Done when: Welcome message is editable from admin panel + +- [x] **T02: Mobile polish and end-to-end verification** `est:30m` + - Why: Table-based queue may have issues at narrow viewports. Need to verify all flows. + - Files: Various frontend components (fixes only as needed) + - Do: Test at 390px viewport width. Fix any overflow, truncation, or touch target issues. Verify: submit download, view queue, cancel download, completed actions, dark/light toggle, footer. Fix any issues found. + - Verify: All flows work at mobile viewport. No horizontal overflow on queue table. + - Done when: Mobile experience is functional and clean + +- [x] **T03: Final test run and cleanup** `est:15m` + - Why: Ensure no regressions across the full M002 milestone + - Files: Test files, cleanup any unused components + - Do: Run full test suites. Remove ThemePicker.vue if no longer imported. Remove DownloadItem.vue if no longer imported. Clean up any dead imports. + - Verify: All tests pass, no unused component files, no dead imports + - Done when: Clean codebase, all tests green + +## Files Likely Touched + +- `frontend/src/components/AdminPanel.vue` (add Settings tab) +- `frontend/src/stores/admin.ts` (add updateSettings) +- `backend/app/routers/admin.py` (add PUT /admin/settings) +- Various frontend components (mobile fixes as needed) +- `frontend/src/components/ThemePicker.vue` (remove if unused) +- `frontend/src/components/DownloadItem.vue` (remove if unused) 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 @@ - - - - - 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, } })