Monitors the availability and latency of HTTPS endpoints. A Spring Boot backend polls each active endpoint on a configurable interval; a React SPA displays live status cards and refreshes every 10 seconds.
- Status cards showing current status (UP/DOWN), latency, uptime percentage, and last-checked time
- Latency leaderboard of the three fastest active endpoints
- Public watchlist management — any visitor can activate or deactivate catalog endpoints without logging in
- Admin custom endpoint tracking — add arbitrary HTTPS URLs via the UI
- Public submission queue — visitors suggest endpoints; admins approve or deny from the dashboard
- Drag-and-drop card ordering, persisted to
localStorage - Optional email notification when a new public submission arrives
- Single-container deployment via multi-stage Docker build
| Layer | Technology |
|---|---|
| Backend | Java 21, Spring Boot 3.5, Spring Data JPA |
| Database (dev) | H2 (in-memory) |
| Database (prod) | PostgreSQL 16 |
| Migrations | Flyway |
| Frontend | React 18, TypeScript, Vite 5, Tailwind CSS |
| Data fetching | @tanstack/react-query v5 |
| Drag-and-drop | @dnd-kit/sortable |
| HTTP client | axios |
| Container | Docker (multi-stage), Docker Compose |
| CI | GitHub Actions |
The lock icon in the top-right corner of the dashboard opens the admin login modal. Submitting the correct API key calls POST /api/auth/ping; the server validates it and sets an httpOnly admin_session cookie with an 8-hour sliding window. On subsequent page loads the frontend calls /api/auth/status to restore admin mode silently. Clicking "Sign out" invalidates the session server-side immediately.
Everything else is public: viewing the dashboard, activating and deactivating catalog endpoints, clearing the watchlist, and submitting endpoint suggestions. This is intentional for a shared dashboard. If you want watchlist operations to be gated, add an authenticated() rule to the relevant routes in SecurityConfig.java.
All URLs submitted for monitoring are validated before the first health check: HTTPS is required, the hostname is checked against private and loopback ranges, and DNS resolution is performed with the resolved IPs re-checked at connection time to close the DNS-rebinding window. The backend applies a rate limit of 120 requests per minute per IP; requests over the limit get a 429. If the app is behind a Cloudflare Tunnel, set RATE_LIMIT_TRUST_PROXY=true so the real client IP is read from the CF-Connecting-IP header. The client-side login lockout (10 failures in 10 minutes triggers a 1-hour lockout stored in localStorage) is UX friction only; a determined attacker can bypass it by clearing storage, so the server-side rate limit is the actual control.
cp .env.example .env
# Set SECRET_SECURITY_KEY and DB_PASSWORD in .env
docker compose up --buildThe app is at http://localhost:8080. The default admin key is whatever you set for SECRET_SECURITY_KEY. The cloudflared service in the Compose file will fail on startup if TUNNEL_TOKEN is blank, but the rest of the stack runs fine.
The dev Spring profile uses H2 in-memory.
# Terminal 1
cd backend
mvn spring-boot:run -Dspring-boot.run.profiles=dev
# Terminal 2
cd frontend
npm install
npm run devFrontend dev server is at http://localhost:5173 and proxies /api to :8080. The admin key in dev mode is dev-api-key (hardcoded in application-dev.properties).
All variables are read from .env by Docker Compose or passed directly as container environment variables.
| Variable | Default | Notes |
|---|---|---|
SECRET_SECURITY_KEY |
dev-api-key |
Admin API key. Required in production. |
DB_PASSWORD |
changeme |
PostgreSQL password. Required in production. |
DB_USERNAME |
apimonitor |
PostgreSQL username. |
CORS_ALLOWED_ORIGINS |
http://localhost:8080 |
Comma-separated list of allowed origins. |
TUNNEL_TOKEN |
(empty) | Cloudflare Tunnel token. Leave blank to skip the cloudflared container. |
MONITOR_CHECK_INTERVAL_MS |
60000 |
Poll interval in milliseconds. |
MAIL_HOST |
(empty) | SMTP hostname. Leave blank to disable email notifications. |
MAIL_PORT |
587 |
SMTP port. |
MAIL_USERNAME |
(empty) | SMTP username. |
MAIL_PASSWORD |
(empty) | SMTP password. |
MAIL_TO |
(empty) | Address that receives new-submission alerts. |
The three variables that actually matter in production are SECRET_SECURITY_KEY, DB_PASSWORD, and CORS_ALLOWED_ORIGINS. The rest can stay at their defaults.
All paths are relative to the server root. "Admin" means the request must include an X-API-Key header on first login, or the admin_session cookie set after a successful login.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/health-metrics |
Public | List all endpoint metrics |
POST |
/api/health-metrics/activate/{id} |
Public | Activate a catalog endpoint |
POST |
/api/health-metrics/deactivate/{id} |
Public | Deactivate a catalog endpoint |
POST |
/api/health-metrics/deactivate/all |
Public | Deactivate all active endpoints |
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/api/custom-endpoints |
Admin | Add a custom HTTPS endpoint |
DELETE |
/api/custom-endpoints/{id} |
Admin | Remove a custom endpoint |
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/api/submissions |
Public | Submit an endpoint suggestion |
GET |
/api/submissions/{token} |
Public | Poll submission status by UUID token |
GET |
/api/submissions |
Admin | List pending submissions |
POST |
/api/submissions/{id}/approve |
Admin | Approve a submission |
POST |
/api/submissions/{id}/deny |
Admin | Deny a submission |
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/api/auth/ping |
Key header | Validate key; sets session cookie on 204, returns 401 on failure |
GET |
/api/auth/status |
Public | Returns {"admin": true} if a valid session cookie is present |
POST |
/api/auth/logout |
Public | Invalidates the session and clears the cookie |