feat. Mini UI to hide ugly 404s
This commit is contained in:
@@ -98,7 +98,7 @@ pytest
|
|||||||
```
|
```
|
||||||
|
|
||||||
Docker compose:
|
Docker compose:
|
||||||
|
> Locally only run this, is pre setup to use the dev compose file.
|
||||||
```bash
|
```bash
|
||||||
docker compose up --build -f docker-compose.dev.yml
|
docker compose up --build -f docker-compose.dev.yml
|
||||||
```
|
```
|
||||||
|
|||||||
2
TODO.md
2
TODO.md
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
### P2 (Nice to Have)
|
### P2 (Nice to Have)
|
||||||
- [x] `FEATURE`: Add a note line at the end of comments to show model tokens used and such.
|
- [x] `FEATURE`: Add a note line at the end of comments to show model tokens used and such.
|
||||||
- [ ] `FEATURE`: Little static tailwind cdn styled page for any http endpoint that just shows what this is, incase this gets discovered by some random lad. Other routes than "/" should return a 404 with if a browser accessed it a again, tailwind cdn themed 404 page. Both should be nicely designed and minimalistic.
|
- [x] `FEATURE`: Little static tailwind cdn styled page for any http endpoint that just shows what this is, incase this gets discovered by some random lad. Other routes than "/" should return a 404 with if a browser accessed it a again, tailwind cdn themed 404 page. Both should be nicely designed and minimalistic.
|
||||||
- [ ] `FEATURE`: Apply `.codex-review.yml` `review.default_mode` when `@codex review` is issued without explicit mode.
|
- [ ] `FEATURE`: Apply `.codex-review.yml` `review.default_mode` when `@codex review` is issued without explicit mode.
|
||||||
- [ ] `FEATURE`: Add per-repo command policy in `.codex-review.yml` for enabling/disabling `review`, `fix`, `explain`, and `rerun` independently.
|
- [ ] `FEATURE`: Add per-repo command policy in `.codex-review.yml` for enabling/disabling `review`, `fix`, `explain`, and `rerun` independently.
|
||||||
- [ ] `TEST`: Add structured log redaction tests to ensure PAT/keys never appear in logs/comments.
|
- [ ] `TEST`: Add structured log redaction tests to ensure PAT/keys never appear in logs/comments.
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ from pathlib import Path
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import Depends, FastAPI, Header, HTTPException, Request, status
|
from fastapi import Depends, FastAPI, Header, HTTPException, Request, status
|
||||||
|
from fastapi.exception_handlers import http_exception_handler
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from gitea_codex_bot.config import Settings, get_settings
|
from gitea_codex_bot.config import Settings, get_settings
|
||||||
@@ -134,6 +137,117 @@ async def lifespan(app: FastAPI):
|
|||||||
app = FastAPI(title="Gitea Codex Review Bot", lifespan=lifespan)
|
app = FastAPI(title="Gitea Codex Review Bot", lifespan=lifespan)
|
||||||
|
|
||||||
|
|
||||||
|
def _render_landing_page() -> str:
|
||||||
|
return """<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Gitea Codex Review Bot</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen bg-slate-950 text-slate-100 antialiased">
|
||||||
|
<main class="mx-auto flex min-h-screen max-w-3xl items-center px-6 py-16">
|
||||||
|
<section class="w-full rounded-2xl border border-slate-800 bg-slate-900/70 p-8 shadow-2xl shadow-slate-950/40 backdrop-blur">
|
||||||
|
<p class="inline-flex rounded-full border border-emerald-400/30 bg-emerald-400/10 px-3 py-1 text-xs font-semibold uppercase tracking-[0.16em] text-emerald-300">Webhook Service</p>
|
||||||
|
<h1 class="mt-4 text-3xl font-semibold tracking-tight text-white sm:text-4xl">Gitea Codex Review Bot</h1>
|
||||||
|
<p class="mt-4 text-base leading-7 text-slate-300">This endpoint powers automated pull request review workflows for Gitea. It validates signed webhook events, queues review jobs, and posts structured feedback back to pull requests.</p>
|
||||||
|
<div class="mt-8 flex flex-wrap gap-3 text-sm">
|
||||||
|
<button id="health-button" type="button" class="rounded-lg border border-slate-700 bg-slate-800/80 px-3 py-2 text-slate-200 transition hover:border-slate-500 hover:bg-slate-700">Health: <code>/healthz</code></button>
|
||||||
|
<span class="rounded-lg border border-slate-700 bg-slate-800/80 px-3 py-2 text-slate-200">Webhook: <code>POST /webhook/gitea</code></span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<div id="health-modal" class="fixed inset-0 z-10 hidden items-center justify-center bg-slate-950/70 px-6">
|
||||||
|
<section class="w-full max-w-md rounded-xl border border-slate-800 bg-slate-900 p-6 shadow-2xl shadow-slate-950/40">
|
||||||
|
<div class="flex items-start justify-between gap-4">
|
||||||
|
<h2 class="text-lg font-semibold text-white">Health Check</h2>
|
||||||
|
<button id="close-modal" type="button" class="rounded-md border border-slate-700 px-2 py-1 text-xs text-slate-300 transition hover:border-slate-500 hover:bg-slate-800">Close</button>
|
||||||
|
</div>
|
||||||
|
<p id="health-result" class="mt-4 text-sm leading-6 text-slate-300">Loading...</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const healthButton = document.getElementById("health-button");
|
||||||
|
const healthModal = document.getElementById("health-modal");
|
||||||
|
const closeModal = document.getElementById("close-modal");
|
||||||
|
const healthResult = document.getElementById("health-result");
|
||||||
|
|
||||||
|
async function loadHealth() {
|
||||||
|
healthResult.textContent = "Loading...";
|
||||||
|
try {
|
||||||
|
const response = await fetch("/healthz", { headers: { Accept: "application/json" } });
|
||||||
|
const payload = await response.json();
|
||||||
|
const statusValue = typeof payload.status === "string" ? payload.status.toLowerCase() : "unknown";
|
||||||
|
const parsedStatus = statusValue === "ok" ? "Healthy" : "Unexpected";
|
||||||
|
healthResult.textContent = "Parsed status: " + parsedStatus + " (raw: " + JSON.stringify(payload) + ")";
|
||||||
|
} catch (_error) {
|
||||||
|
healthResult.textContent = "Could not load health check output.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showModal() {
|
||||||
|
healthModal.classList.remove("hidden");
|
||||||
|
healthModal.classList.add("flex");
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideModal() {
|
||||||
|
healthModal.classList.add("hidden");
|
||||||
|
healthModal.classList.remove("flex");
|
||||||
|
}
|
||||||
|
|
||||||
|
healthButton.addEventListener("click", async function () {
|
||||||
|
showModal();
|
||||||
|
await loadHealth();
|
||||||
|
});
|
||||||
|
|
||||||
|
closeModal.addEventListener("click", hideModal);
|
||||||
|
healthModal.addEventListener("click", function (event) {
|
||||||
|
if (event.target === healthModal) {
|
||||||
|
hideModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
|
||||||
|
def _render_browser_404_page() -> str:
|
||||||
|
return """<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Not Found</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen bg-slate-950 text-slate-100 antialiased">
|
||||||
|
<main class="mx-auto flex min-h-screen max-w-2xl items-center px-6 py-16">
|
||||||
|
<section class="w-full rounded-2xl border border-slate-800 bg-slate-900/70 p-8 text-center shadow-2xl shadow-slate-950/40 backdrop-blur">
|
||||||
|
<p class="text-sm font-medium uppercase tracking-[0.2em] text-slate-400">Error 404</p>
|
||||||
|
<h1 class="mt-3 text-3xl font-semibold text-white">Page not found</h1>
|
||||||
|
<p class="mt-4 text-slate-300">This service exposes only a small set of routes. Head back to the home page for a quick overview.</p>
|
||||||
|
<a href="/" class="mt-8 inline-flex rounded-lg border border-slate-700 bg-slate-800 px-4 py-2 text-sm font-medium text-slate-100 transition hover:border-slate-500 hover:bg-slate-700">Go to home</a>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
|
||||||
|
@app.exception_handler(StarletteHTTPException)
|
||||||
|
async def custom_http_exception_handler(request: Request, exc: StarletteHTTPException):
|
||||||
|
if exc.status_code == status.HTTP_404_NOT_FOUND:
|
||||||
|
accept = request.headers.get("accept", "")
|
||||||
|
if "text/html" in accept.lower():
|
||||||
|
return HTMLResponse(content=_render_browser_404_page(), status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
return await http_exception_handler(request, exc)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/", response_class=HTMLResponse)
|
||||||
|
def root() -> str:
|
||||||
|
return _render_landing_page()
|
||||||
|
|
||||||
|
|
||||||
@app.get("/healthz")
|
@app.get("/healthz")
|
||||||
def healthz(settings: Settings = Depends(get_settings)) -> dict[str, str]:
|
def healthz(settings: Settings = Depends(get_settings)) -> dict[str, str]:
|
||||||
_ = settings.gitea_base_url
|
_ = settings.gitea_base_url
|
||||||
|
|||||||
40
tests/test_main_pages.py
Normal file
40
tests/test_main_pages.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from gitea_codex_bot.main import app
|
||||||
|
|
||||||
|
|
||||||
|
def test_root_returns_tailwind_landing_page() -> None:
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
response = client.get("/")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.headers["content-type"].startswith("text/html")
|
||||||
|
assert "Gitea Codex Review Bot" in response.text
|
||||||
|
assert "cdn.tailwindcss.com" in response.text
|
||||||
|
assert 'id="health-button"' in response.text
|
||||||
|
assert 'id="health-modal"' in response.text
|
||||||
|
assert 'fetch("/healthz"' in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_404_returns_tailwind_page_for_browser_requests() -> None:
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
response = client.get("/missing", headers={"Accept": "text/html"})
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
assert response.headers["content-type"].startswith("text/html")
|
||||||
|
assert "Error 404" in response.text
|
||||||
|
assert "cdn.tailwindcss.com" in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_404_returns_json_for_non_browser_requests() -> None:
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
response = client.get("/missing", headers={"Accept": "application/json"})
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
assert response.headers["content-type"].startswith("application/json")
|
||||||
|
assert response.json() == {"detail": "Not Found"}
|
||||||
Reference in New Issue
Block a user