From 0bb0975a09b3d7a9a3d1fbb0ee5d87f63b6cc4c2 Mon Sep 17 00:00:00 2001 From: "thomas.kopp" Date: Wed, 1 Apr 2026 14:09:23 +0200 Subject: [PATCH] docs: add transcript modal + delete implementation plan --- .../2026-04-01-transcript-modal-delete.md | 383 ++++++++++++++++++ 1 file changed, 383 insertions(+) create mode 100644 docs/plans/2026-04-01-transcript-modal-delete.md diff --git a/docs/plans/2026-04-01-transcript-modal-delete.md b/docs/plans/2026-04-01-transcript-modal-delete.md new file mode 100644 index 0000000..cccfda6 --- /dev/null +++ b/docs/plans/2026-04-01-transcript-modal-delete.md @@ -0,0 +1,383 @@ +# 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 `