Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

RouteForge is a read-only routing preflight and explainability tool for BGP, RPKI, Registry/IRR and Routing Visibility checks.

Current user-facing version: **v0.5.0-beta**.
Current user-facing version: **v0.5.2-beta**.

<!-- Screenshot gallery placeholder:
- docs/screenshots/dashboard.png
Expand Down Expand Up @@ -32,7 +32,7 @@ Routing changes often require fast but traceable checks across multiple external

## Current Alpha Status

RouteForge is a **functional alpha** release with production-like workflows for read-only validation and demo usage. Current release target: **v0.5.0-beta**.
RouteForge is a **functional alpha** release with production-like workflows for read-only validation and demo usage. Current release target: **v0.5.2-beta**.

## Quickstart with Docker Compose

Expand All @@ -51,6 +51,29 @@ URLs:
- System Info (via frontend proxy): http://localhost:3000/api/system/info
- Direct System Info (backend): http://localhost:8000/api/system/info


## System Status

RouteForge exposes runtime operational checks via `GET /api/system/status` and a **System** page in the GUI.

Visible information includes:
- Version
- Mode (live/demo)
- Read-only safety state
- Database status
- RIPEstat runtime settings
- Enabled core features

```bash
curl http://localhost:3000/api/system/status
```

or directly against backend:

```bash
curl http://localhost:8000/api/system/status
```

## Demo Mode

Set `ROUTEFORGE_DEMO_MODE=true` to use fixed demo data.
Expand Down
23 changes: 23 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
# Release Notes

## v0.5.2-beta

**System Status & Operational Checks**

### Highlights

- New `/api/system/status` endpoint
- GUI System Status view
- Database health visibility
- RIPEstat runtime settings visibility
- Read-only safety state shown
- API proxy status visible from frontend
- Improved selfhosting observability

### Known limitations

- No authentication yet
- No multi-user support yet
- No advanced alerting
- No scheduled health checks yet

---

## v0.5.0-beta

**Production Selfhosting Foundation**
Expand Down
5 changes: 4 additions & 1 deletion backend/app/api/routes_health.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from fastapi import APIRouter

from app.core.system_status import get_database_status
from app.database import engine

router = APIRouter()


@router.get('/health')
def health() -> dict:
return {"status": "ok", "version": "v0.5.0-beta"}
return {"status": "ok", "version": "v0.5.2-beta", "database": get_database_status(engine).get("status", "unknown")}
9 changes: 8 additions & 1 deletion backend/app/api/routes_system.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from fastapi import APIRouter

from app.config import settings
from app.core.system_status import build_system_status
from app.database import engine

router = APIRouter(prefix='/api/system', tags=['system'])

Expand All @@ -9,8 +11,13 @@
def system_info():
return {
'name': 'RouteForge',
'version': 'v0.5.0-beta',
'version': 'v0.5.2-beta',
'demo_mode': settings.demo_mode,
'read_only': True,
'data_sources': ['RIPEstat', 'RIPEstat Whois/Registry'],
}


@router.get('/status')
def system_status():
return build_system_status(engine)
96 changes: 96 additions & 0 deletions backend/app/core/system_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from __future__ import annotations

from urllib.parse import urlsplit, urlunsplit

from sqlalchemy import text
from sqlalchemy.engine import Engine

from app.config import settings


def safe_database_url(database_url: str) -> str:
if not database_url:
return "unknown"
try:
parsed = urlsplit(database_url)
if parsed.scheme.startswith("sqlite"):
return database_url
if not parsed.scheme or not parsed.netloc:
return "configured"

host = parsed.hostname or ""
port = f":{parsed.port}" if parsed.port else ""
user = parsed.username or ""
auth = f"{user}@" if user else ""
netloc = f"{auth}{host}{port}"
scheme = parsed.scheme.split("+", 1)[0]
return urlunsplit((scheme, netloc, parsed.path, parsed.query, parsed.fragment))
except Exception:
return "configured"


def database_type_from_url(database_url: str) -> str:
if not database_url:
return "unknown"
scheme = (urlsplit(database_url).scheme or "unknown").split("+", 1)[0]
return scheme or "unknown"


def _safe_error_message(exc: Exception) -> str:
message = str(exc)
db_url = settings.database_url
sanitized = safe_database_url(db_url)
if db_url:
message = message.replace(db_url, "[database-url]")
if sanitized and sanitized != db_url:
message = message.replace(sanitized, "[database-url]")
return message[:300]


def get_database_status(engine: Engine | None) -> dict:
db_url = settings.database_url
payload = {
"status": "unknown",
"type": database_type_from_url(db_url),
"url_safe": safe_database_url(db_url),
}
if engine is None:
return payload
try:
with engine.connect() as connection:
connection.execute(text("SELECT 1"))
payload["status"] = "ok"
except Exception as exc:
payload["status"] = "error"
payload["error_message"] = _safe_error_message(exc)
return payload


def build_system_status(engine: Engine | None) -> dict:
return {
"status": "ok",
"name": settings.app_name,
"version": "v0.5.2-beta",
"read_only": True,
"mode": "demo" if settings.demo_mode else "live",
"demo_mode": settings.demo_mode,
"database": get_database_status(engine),
"api_proxy": {"status": "ok", "mode": "same-origin", "frontend_proxy_expected": True},
"ripestat": {
"cache_ttl_seconds": settings.cache_ttl_seconds,
"timeout_seconds": settings.ripestat_timeout_seconds,
"max_retries": settings.ripestat_max_retries,
"retry_backoff_seconds": settings.ripestat_retry_backoff_seconds,
"use_stale_cache_on_error": settings.ripestat_use_stale_cache_on_error,
},
"features": {
"asn_check": True,
"prefix_check": True,
"preflight": True,
"reports": True,
"exports": True,
"data_source_diagnostics": True,
"cache_freshness": True,
"retry_resilience": True,
},
}
2 changes: 1 addition & 1 deletion backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from app.config import settings
from app.database import Base, engine

app = FastAPI(title="RouteForge", version="0.5.0")
app = FastAPI(title="RouteForge", version="0.5.2")

app.add_middleware(
CORSMiddleware,
Expand Down
2 changes: 1 addition & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "routeforge-backend"
version = "0.5.0"
version = "0.5.2"
description = "RouteForge backend"
requires-python = ">=3.12"
dependencies = [
Expand Down
21 changes: 21 additions & 0 deletions backend/tests/test_api_smoke.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import app.config as config
from app.core.system_status import safe_database_url

import importlib

from fastapi.testclient import TestClient
Expand Down Expand Up @@ -147,3 +150,21 @@ def test_report_export_not_found() -> None:
response = client.get(f'/api/reports/999999/{endpoint}')
assert response.status_code == 404
assert response.json().get('detail') == 'Report not found'


def test_system_status_endpoint() -> None:
client = _client()
response = client.get('/api/system/status')
assert response.status_code == 200
payload = response.json()
assert payload.get('version') == 'v0.5.2-beta'
assert payload.get('read_only') is True
assert payload.get('database', {}).get('status')
assert payload.get('ripestat', {}).get('cache_ttl_seconds') is not None
assert payload.get('features', {}).get('preflight') is True


def test_safe_database_url() -> None:
assert safe_database_url('postgresql+psycopg://routeforge:secret@postgres:5432/routeforge') == 'postgresql://routeforge@postgres:5432/routeforge'
assert safe_database_url('sqlite:////app/data/routeforge.db') == 'sqlite:////app/data/routeforge.db'
assert safe_database_url('not a url') == 'configured'
29 changes: 29 additions & 0 deletions docs/operations/system-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# System Status

RouteForge provides operational visibility through the **System** page in the UI and the endpoint `GET /api/system/status`.

## Endpoints

- `GET /api/system/status`: detailed runtime/operational status.
- `GET /health`: lightweight liveness status (compatible baseline endpoint).

## What to check

### If `database=status:error`
- Verify `DATABASE_URL` in `.env`.
- Verify database container/service availability.
- Verify network routing between backend and database.
- Verify credentials and database existence.

### If API Proxy shows `ERROR` in UI
- Verify frontend nginx is running.
- Verify `/api` proxy config in frontend nginx.
- Verify backend service is reachable from frontend container.
- Verify browser calls use same-origin `/api/...`.

### Demo Mode
- Check `demo_mode` field in `/api/system/status`.
- Check `ROUTEFORGE_DEMO_MODE` runtime value.

### Version
- Validate expected deployed version in `/api/system/status` and `/health`.
4 changes: 2 additions & 2 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "routeforge-frontend",
"version": "0.5.0",
"version": "0.5.2",
"private": true,
"type": "module",
"scripts": {
Expand Down
Loading
Loading