diff --git a/frontend/index.html b/frontend/index.html
index 0dad9a2..4de7e07 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -61,15 +61,24 @@ textarea { resize: vertical; min-height: 120px; }
.work-filter[aria-pressed="true"] { border-color:var(--accent); background:#1d4f7a; }
.my-work-list { display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:10px; }
.my-work-card { min-height: 44px; display:block; padding:12px; border:1px solid #1f3a5f; border-radius:12px; background:#0f1d33; color:var(--text); }
+.my-work-card.review-trigger { width:100%; text-align:left; font:inherit; }
.my-work-card:hover { border-color:var(--accent); }
.my-work-card-title { display:block; margin:5px 0; font-weight:650; }
.my-work[data-stale="true"] { border-color:#fcd34d; }
+.review-sheet { position:fixed; inset:0; z-index:50; display:none; justify-content:flex-end; background:rgba(5,12,21,.72); backdrop-filter:blur(4px); }
+.review-sheet.open { display:flex; }
+.review-sheet-panel { width:min(560px,100%); height:100%; overflow:auto; padding:18px; background:#0b1526; border-left:1px solid #2a496e; }
+.review-sheet-header { display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap; }
+.review-sheet-body { white-space:pre-wrap; overflow-wrap:anywhere; }
+.review-file, .review-history { padding:8px 0; border-bottom:1px solid #1b2d45; overflow-wrap:anywhere; }
+.review-action { min-height:44px; }
@media (max-width: 600px) {
header { align-items:flex-start; }
.my-work { margin:0; }
.my-work-list { grid-template-columns:1fr; }
.work-filters { width:100%; }
.work-filter { flex:1 1 calc(50% - 8px); }
+ .review-sheet-panel { width:100%; border-left:0; padding:14px; }
}
.obi { width:14px; height:14px; background: url('data:image/svg+xml;utf8,') center/contain no-repeat; display:inline-block; }
.footer { padding: 12px; text-align: center; color:#4e6b8a; font-size:12px; }
@@ -189,12 +198,32 @@ textarea { resize: vertical; min-height: 120px; }
+
+
+
+ Choose a review request.
+
+
+ Changed files
+
+ Review history
+
+
+
+
+
' in html
+ assert '@media (max-width: 600px)' in html
+ assert '.review-sheet-panel' in html and 'width:100%' in html
+ assert '.review-action' in html and 'min-height:44px' in html
+
+
+@pytest.mark.anyio
+async def test_review_sheet_loads_details_and_preserves_safe_gitea_handoff():
+ html = await dashboard()
+
+ assert 'data-review-index' in html
+ assert "reviewController.load(selectedReview)" in html
+ assert "review-files" in html
+ assert "review-history" in html
+ assert "open-review-gitea" in html
+ assert "reviewController.submit" not in html
diff --git a/tests/test_review_api.py b/tests/test_review_api.py
new file mode 100644
index 0000000..fda4ed8
--- /dev/null
+++ b/tests/test_review_api.py
@@ -0,0 +1,61 @@
+import httpx
+import pytest
+
+from src import main
+
+
+@pytest.mark.anyio
+async def test_review_detail_endpoint_returns_normalized_no_store_payload(monkeypatch):
+ async def requested(repository, number):
+ return (repository, number) == ("stackchain/api", 7)
+
+ async def detail(repository, number):
+ assert (repository, number) == ("stackchain/api", 7)
+ return {
+ "repository": repository,
+ "number": number,
+ "title": "Review API",
+ "body": "Check retries",
+ "url": "https://forge.example/stackchain/api/pulls/7",
+ "author": "alex",
+ "ci_state": "success",
+ "files": [{"filename": "src/api.py", "status": "modified", "additions": 8, "deletions": 2}],
+ "reviews": [{"user": {"login": "sam"}, "state": "APPROVED", "body": "Good"}],
+ }
+
+ monkeypatch.setattr(main, "is_requested_review", requested)
+ monkeypatch.setattr(main, "pull_review_detail", detail)
+ transport = httpx.ASGITransport(app=main.app)
+ async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
+ response = await client.get("/api/v1/repos/stackchain/api/pulls/7/review")
+
+ assert response.status_code == 200
+ assert response.headers["cache-control"] == "no-store"
+ assert response.json()["files"][0]["filename"] == "src/api.py"
+
+
+@pytest.mark.anyio
+async def test_review_detail_rejects_pulls_not_requested_from_service_user(monkeypatch):
+ async def requested(repository, number):
+ return False
+
+ monkeypatch.setattr(main, "is_requested_review", requested)
+ transport = httpx.ASGITransport(app=main.app)
+ async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
+ response = await client.get("/api/v1/repos/private/secret/pulls/9/review")
+
+ assert response.status_code == 404
+ assert response.headers["cache-control"] == "no-store"
+
+
+@pytest.mark.anyio
+async def test_public_review_endpoint_does_not_expose_service_token_mutations():
+ transport = httpx.ASGITransport(app=main.app)
+ async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
+ response = await client.post(
+ "/api/v1/repos/stackchain/api/pulls/7/review",
+ json={"action": "approve"},
+ )
+
+ assert response.status_code == 405
+