feat. Mini UI to hide ugly 404s

This commit is contained in:
Space-Banane
2026-05-22 22:28:11 +02:00
parent 7067f10f10
commit 33e8fe9315
4 changed files with 156 additions and 2 deletions

View File

@@ -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
``` ```

View File

@@ -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.

View File

@@ -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
View 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"}