From e74c89e1b094219bd0262a737927ea746b16def6 Mon Sep 17 00:00:00 2001 From: Kagamine Rin Date: Fri, 29 May 2026 20:21:05 -0400 Subject: [PATCH] feat: Settings modal with danger zone admin operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - POST /api/admin/clear-waveforms — deletes all cached waveform SVGs - POST /api/admin/delete-all-clips — nuclear option: deletes all clips, MP3s, artwork, and waveforms. Returns deleted count. - database.py: delete_all_clips() for bulk DB wipe - Clip deletion cascade-removes waveform SVGs Frontend: - SettingsModal.tsx: center-stage modal with backdrop blur (bg-black/60 backdrop-blur-sm). Danger zone with red-styled border and header. - Two-step confirmation for Delete All (click once to arm, click again to confirm). Shows clip count dynamically. - Gear icon trigger in header bar - App.tsx: handleClearWaveforms + handleDeleteAll handlers Docs: - Architecture KB updated with album art, waveform, settings modal sections - Tasks KB updated with T-021 (waveform) and TASK-001 (album art) marked done - T-026 (settings modal) added to backlog --- backend/src/database.py | 5 + backend/src/main.py | 38 +++- .../7239af1a-2a3f-46bd-816a-2e349752cb83.svg | 203 ------------------ frontend/src/App.tsx | 53 ++++- frontend/src/components/SettingsModal.tsx | 129 +++++++++++ 5 files changed, 219 insertions(+), 209 deletions(-) delete mode 100644 data/waveforms/7239af1a-2a3f-46bd-816a-2e349752cb83.svg create mode 100644 frontend/src/components/SettingsModal.tsx diff --git a/backend/src/database.py b/backend/src/database.py index 4dac2f1..b20bc0e 100644 --- a/backend/src/database.py +++ b/backend/src/database.py @@ -151,4 +151,9 @@ async def remove_artwork(clip_id: str): async def delete_clip(clip_id: str): async with aiosqlite.connect(DB_PATH) as db: await db.execute("DELETE FROM clips WHERE id = ?", (clip_id,)) + await db.commit() + +async def delete_all_clips(): + async with aiosqlite.connect(DB_PATH) as db: + await db.execute("DELETE FROM clips") await db.commit() \ No newline at end of file diff --git a/backend/src/main.py b/backend/src/main.py index d9d9aa6..ca31a07 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -10,7 +10,7 @@ from dotenv import load_dotenv load_dotenv(Path(__file__).parent.parent / ".env") from models import GenerateRequest, GenerateResponse, ClipList, RenameRequest, LYRIA_MODELS, DEFAULT_MODEL -from database import init_db, create_clip, get_clips, get_clip, delete_clip, update_clip_status, rename_clip, set_artwork, remove_artwork +from database import init_db, create_clip, get_clips, get_clip, delete_clip, update_clip_status, rename_clip, set_artwork, remove_artwork, delete_all_clips from lyria_client import generate_clip, save_clip_file from waveform import generate_waveform_svg, get_waveform_path @@ -226,6 +226,42 @@ async def rename_clip_endpoint(clip_id: str, request: RenameRequest): updated = await get_clip(clip_id) return updated +# ─── Admin Endpoints ────────────────────────────────────────────── + +@app.post("/api/admin/clear-waveforms") +async def clear_waveforms(): + """Delete all cached waveform SVGs. They regenerate on next view.""" + from waveform import WAVEFORM_DIR + count = 0 + if WAVEFORM_DIR.exists(): + for f in WAVEFORM_DIR.glob("*.svg"): + f.unlink() + count += 1 + return {"cleared": count} + +@app.post("/api/admin/delete-all-clips") +async def delete_all_clips_endpoint(): + """Delete all clips, MP3s, artwork, and waveforms. Nuclear option.""" + # Get all clips to clean up files + all_clips = await get_clips() + for clip in all_clips: + # Delete MP3 + mp3_path = CLIPS_DIR / clip["filename"] + if mp3_path.exists(): + mp3_path.unlink() + # Delete artwork + if clip.get("artwork_filename"): + art_path = ARTWORK_DIR / clip["artwork_filename"] + if art_path.exists(): + art_path.unlink() + # Delete waveform + wf_path = get_waveform_path(clip["id"]) + if wf_path and wf_path.exists(): + wf_path.unlink() + # Clear DB + await delete_all_clips() + return {"deleted": len(all_clips)} + if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/data/waveforms/7239af1a-2a3f-46bd-816a-2e349752cb83.svg b/data/waveforms/7239af1a-2a3f-46bd-816a-2e349752cb83.svg deleted file mode 100644 index f4aa175..0000000 --- a/data/waveforms/7239af1a-2a3f-46bd-816a-2e349752cb83.svg +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f0dd840..3fcbaa3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,6 +3,7 @@ import Generator from './components/Generator' import Gallery from './components/Gallery' import Player from './components/Player' import DetailPanel from './components/DetailPanel' +import SettingsModal from './components/SettingsModal' interface Clip { id: string @@ -22,6 +23,7 @@ function App() { const [currentClip, setCurrentClip] = useState(null) const [isPlaying, setIsPlaying] = useState(false) const [selectedClip, setSelectedClip] = useState(null) + const [settingsOpen, setSettingsOpen] = useState(false) const fetchClips = async () => { try { @@ -117,15 +119,48 @@ function App() { } } + const handleClearWaveforms = async () => { + try { + await fetch('/api/admin/clear-waveforms', { method: 'POST' }) + } catch (err) { + console.error('Failed to clear waveforms:', err) + } + } + + const handleDeleteAll = async () => { + try { + await fetch('/api/admin/delete-all-clips', { method: 'POST' }) + setClips([]) + setCurrentClip(null) + setIsPlaying(false) + setSelectedClip(null) + } catch (err) { + console.error('Failed to delete all clips:', err) + } + } + return (
-
-
- L +
+
+
+ L +
+

Lyria Studio

+ v0.1
-

Lyria Studio

- v0.1 +
@@ -168,6 +203,14 @@ function App() { onRemoveArtwork={handleRemoveArtwork} /> )} + + setSettingsOpen(false)} + onClearWaveforms={handleClearWaveforms} + onDeleteAll={handleDeleteAll} + clipCount={clips.length} + />
) } diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx new file mode 100644 index 0000000..dfcde65 --- /dev/null +++ b/frontend/src/components/SettingsModal.tsx @@ -0,0 +1,129 @@ +import { useState } from 'react' + +interface Props { + isOpen: boolean + onClose: () => void + onClearWaveforms: () => Promise + onDeleteAll: () => Promise + clipCount: number +} + +export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDeleteAll, clipCount }: Props) { + const [confirmDelete, setConfirmDelete] = useState(false) + const [loading, setLoading] = useState(false) + + if (!isOpen) return null + + const handleClearWaveforms = async () => { + setLoading(true) + try { + await onClearWaveforms() + } finally { + setLoading(false) + } + } + + const handleDeleteAll = async () => { + if (!confirmDelete) { + setConfirmDelete(true) + return + } + setLoading(true) + try { + await onDeleteAll() + setConfirmDelete(false) + } finally { + setLoading(false) + } + } + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Header */} +
+

Settings

+ +
+ + {/* Content */} +
+ {/* Danger Zone */} +
+
+
+ + + + Danger Zone +
+
+ +
+ {/* Clear Waveforms */} +
+
+

Clear Waveform Cache

+

Delete all cached SVGs. They regenerate on next view.

+
+ +
+ +
+ + {/* Delete All */} +
+
+
+

Delete All Tracks

+

+ Remove {clipCount} clip{clipCount !== 1 ? 's' : ''}, MP3s, artwork, and waveforms. Cannot be undone. +

+
+ +
+ + {confirmDelete && ( +

+ Click again to confirm. This permanently deletes everything. +

+ )} +
+
+
+
+
+
+ ) +}