# Transcript Modal & Delete Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Add transcript delete and a markdown-rendering modal viewer, removing the existing preview section. **Architecture:** Two new REST endpoints (GET + DELETE `/transcripts/{filename}`) with path-confinement security. Frontend gains a full-screen modal using marked.js + DOMPurify for safe rendering; the static preview div is removed entirely. Each list item gets a trash icon that stops event propagation so it doesn't trigger the modal. **Tech Stack:** FastAPI (existing), marked.js 14 + DOMPurify 3 via CDN, vanilla JS/CSS (no new build step) --- ### Task 1: Backend — GET /transcripts/{filename} **Files:** - Modify: `api/router.py` - Modify: `output.py` - Test: `tests/test_api.py` **Step 1: Write the failing test** Add to `tests/test_api.py`: ```python def test_get_transcript_returns_content(tmp_path, monkeypatch): f = tmp_path / "2026-01-01-0900-test.md" f.write_text("# Hello\n\ncontent here\n") from unittest.mock import patch with patch("api.router.current_user", return_value={"username": "", "output_dir": str(tmp_path), "is_admin": False}): from fastapi.testclient import TestClient from main import app client = TestClient(app) r = client.get("/transcripts/2026-01-01-0900-test.md", headers={"Authorization": "Bearer fake"}) assert r.status_code == 200 assert "Hello" in r.text def test_get_transcript_rejects_path_traversal(tmp_path): from unittest.mock import patch with patch("api.router.current_user", return_value={"username": "", "output_dir": str(tmp_path), "is_admin": False}): from fastapi.testclient import TestClient from main import app client = TestClient(app) r = client.get("/transcripts/..%2Fsecret.md", headers={"Authorization": "Bearer fake"}) assert r.status_code == 404 ``` **Step 2: Run to verify it fails** ```bash pytest tests/test_api.py::test_get_transcript_returns_content tests/test_api.py::test_get_transcript_rejects_path_traversal -v ``` Expected: FAIL — 404 or 405 (route doesn't exist yet) **Step 3: Add `read_transcript` to `output.py`** ```python def read_transcript(output_dir: str, filename: str) -> str | None: """Return file content if filename is a plain .md file inside output_dir.""" if os.path.basename(filename) != filename or not filename.endswith(".md"): return None path = os.path.join(output_dir, filename) if not os.path.exists(path): return None with open(path, encoding="utf-8") as f: return f.read() ``` **Step 4: Add GET endpoint to `api/router.py`** Add after the existing `get_transcripts` endpoint: ```python @router.get("/transcripts/{filename}") async def get_transcript(filename: str, user: dict = Depends(current_user)): from output import read_transcript from fastapi.responses import PlainTextResponse user_dir = os.path.join(user["output_dir"], user["username"]) content = read_transcript(user_dir, filename) if content is None: raise HTTPException(status_code=404, detail="Nicht gefunden") return PlainTextResponse(content) ``` **Step 5: Run tests** ```bash pytest tests/test_api.py::test_get_transcript_returns_content tests/test_api.py::test_get_transcript_rejects_path_traversal -v ``` Expected: PASS **Step 6: Commit** ```bash git add output.py api/router.py tests/test_api.py git commit -m "feat: GET /transcripts/{filename} — serve transcript content" ``` --- ### Task 2: Backend — DELETE /transcripts/{filename} **Files:** - Modify: `api/router.py` - Test: `tests/test_api.py` **Step 1: Write the failing tests** Add to `tests/test_api.py`: ```python def test_delete_transcript_removes_file(tmp_path): f = tmp_path / "2026-01-01-0900-test.md" f.write_text("content") from unittest.mock import patch with patch("api.router.current_user", return_value={"username": "", "output_dir": str(tmp_path), "is_admin": False}): from fastapi.testclient import TestClient from main import app client = TestClient(app) r = client.delete("/transcripts/2026-01-01-0900-test.md", headers={"Authorization": "Bearer fake"}) assert r.status_code == 200 assert not f.exists() def test_delete_transcript_rejects_path_traversal(tmp_path): from unittest.mock import patch with patch("api.router.current_user", return_value={"username": "", "output_dir": str(tmp_path), "is_admin": False}): from fastapi.testclient import TestClient from main import app client = TestClient(app) r = client.delete("/transcripts/..%2Fsecret.md", headers={"Authorization": "Bearer fake"}) assert r.status_code == 404 ``` **Step 2: Run to verify they fail** ```bash pytest tests/test_api.py::test_delete_transcript_removes_file tests/test_api.py::test_delete_transcript_rejects_path_traversal -v ``` Expected: FAIL — route doesn't exist **Step 3: Add DELETE endpoint to `api/router.py`** ```python @router.delete("/transcripts/{filename}") async def delete_transcript(filename: str, user: dict = Depends(current_user)): user_dir = os.path.join(user["output_dir"], user["username"]) if os.path.basename(filename) != filename or not filename.endswith(".md"): raise HTTPException(status_code=404, detail="Nicht gefunden") path = os.path.join(user_dir, filename) if not os.path.exists(path): raise HTTPException(status_code=404, detail="Nicht gefunden") os.unlink(path) return {"ok": True} ``` **Step 4: Run tests** ```bash pytest tests/test_api.py::test_delete_transcript_removes_file tests/test_api.py::test_delete_transcript_rejects_path_traversal -v ``` Expected: PASS **Step 5: Commit** ```bash git add api/router.py tests/test_api.py git commit -m "feat: DELETE /transcripts/{filename} — delete transcript with path-confinement check" ``` --- ### Task 3: Frontend — Remove preview section, add modal + marked.js + DOMPurify **Files:** - Modify: `frontend/index.html` - Modify: `frontend/app.js` No automated tests; manual verification checklist at end. **Step 1: Remove preview section from `index.html`** Delete this block from `
`: ```html
Noch keine Aufnahme verarbeitet.
``` Delete these CSS rules (search for them by selector): - `.preview-section` - `#preview` - `#preview.has-content` **Step 2: Add script tags — replace existing ` ``` **Step 3: Add modal HTML + CSS** Add this block inside `
` before `
` closing tag (it's a fixed overlay, position doesn't matter): ```html ``` Add CSS inside `