From 024fe9a20a43f56b7c395eb64726a6cd14acaaa4 Mon Sep 17 00:00:00 2001 From: Gokul Krishna Date: Tue, 25 Aug 2026 03:11:12 +0530 Subject: [PATCH] feat: add web inspection interface --- README.md | 6 +- docs/contribcheck-ui.png | Bin 0 -> 42648 bytes src/contribcheck/api.py | 174 +++++++++++++++++++++++++++++++++++-- src/contribcheck/models.py | 3 +- tests/test_api.py | 11 +++ 5 files changed, 185 insertions(+), 9 deletions(-) create mode 100644 docs/contribcheck-ui.png 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 0000000000000000000000000000000000000000..2ede42f6db04ec9027c55e0916eb6d8bab479b66 GIT binary patch literal 42648 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%0Dq8z6S^``ucN$`|`P$ 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 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: