diff --git a/README.md b/README.md
index 75f0f66..7902352 100644
--- a/README.md
+++ b/README.md
@@ -201,7 +201,11 @@ pip install -e '.[server]'
contribcheck serve
```
-Open `http://127.0.0.1:8000/docs`, or call the API directly:
+Open `http://127.0.0.1:8000/` for the browser interface or `http://127.0.0.1:8000/docs` for the interactive API documentation. The browser interface sends only the issue reference and optional actor to the local API. It does not accept, store, or log GitHub tokens.
+
+
+
+Call the API directly:
```bash
curl -X POST http://127.0.0.1:8000/v1/inspect \
diff --git a/docs/contribcheck-ui.png b/docs/contribcheck-ui.png
new file mode 100644
index 0000000..2ede42f
Binary files /dev/null and b/docs/contribcheck-ui.png differ
diff --git a/src/contribcheck/api.py b/src/contribcheck/api.py
index fd9ed92..096e490 100644
--- a/src/contribcheck/api.py
+++ b/src/contribcheck/api.py
@@ -7,7 +7,7 @@
import httpx
from fastapi import FastAPI, Header, HTTPException
-from fastapi.responses import RedirectResponse
+from fastapi.responses import HTMLResponse
from contribcheck import __version__
from contribcheck.analyzer import IssueAnalyzer
@@ -21,12 +21,172 @@
description="Evidence-based preflight checks for open-source GitHub issues.",
)
-
-@app.get("/", include_in_schema=False)
-async def root() -> RedirectResponse:
- """Send browser users directly to the interactive API documentation."""
-
- return RedirectResponse(url="/docs")
+WEB_PAGE = """
+
+
+
+
+ ContribCheck
+
+
+
+
+ ContribCheck
+ Evidence-based preflight checks for open-source GitHub issues.
+
+
+
+
+
+"""
+
+
+@app.get("/", response_class=HTMLResponse, include_in_schema=False)
+async def root() -> HTMLResponse:
+ """Serve the lightweight browser inspection interface."""
+
+ return HTMLResponse(WEB_PAGE)
@app.get("/health")
diff --git a/src/contribcheck/models.py b/src/contribcheck/models.py
index b884524..d0519c8 100644
--- a/src/contribcheck/models.py
+++ b/src/contribcheck/models.py
@@ -6,7 +6,7 @@
from enum import StrEnum
from typing import Any, Literal
-from pydantic import BaseModel, ConfigDict, Field, HttpUrl
+from pydantic import BaseModel, ConfigDict, Field, HttpUrl, computed_field
class StrictModel(BaseModel):
@@ -47,6 +47,7 @@ def full_name(self) -> str:
return f"{self.owner}/{self.repository}"
+ @computed_field # type: ignore[prop-decorator]
@property
def url(self) -> str:
"""Return the canonical browser URL."""
diff --git a/tests/test_api.py b/tests/test_api.py
index 21ada48..d81a211 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -26,6 +26,16 @@ async def test_health_endpoint() -> None:
assert response.json()["status"] == "ok"
+async def test_root_serves_web_interface() -> None:
+ async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
+ response = await client.get("/")
+
+ assert response.status_code == 200
+ assert "ContribCheck" in response.text
+ assert "/v1/inspect" in response.text
+ assert "Authorization" not in response.text
+
+
async def test_inspect_endpoint_returns_typed_report(
monkeypatch: pytest.MonkeyPatch,
) -> None:
@@ -46,6 +56,7 @@ async def fake_inspect(
assert response.status_code == 200
assert response.json()["status"] == "ready"
assert response.json()["target"]["number"] == 7
+ assert response.json()["target"]["url"] == "https://github.com/owner/repo/issues/7"
async def test_inspect_endpoint_rejects_invalid_reference() -> None: