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. + +![ContribCheck web interface](docs/contribcheck-ui.png) + +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: