RouteForge v0.6.2-beta
Modernes read-only Operator-Tool für Preflight Checks von ASN, Prefix, RPKI und Registry/IRR.
From 2da70aff4408504ab27af21360e7c530c3c3eb7c Mon Sep 17 00:00:00 2001
From: DeepZone
- Modernes read-only Operator-Tool für Preflight Checks von ASN, Prefix, RPKI und Registry/IRR. Modernes read-only Operator-Tool für Preflight Checks von ASN, Prefix, RPKI und Registry/IRR. Version: v0.6.2-beta Version: v0.6.3-beta
+
@@ -43,7 +43,7 @@ Routing changes often require fast but traceable checks across multiple external
## Current Alpha Status
-RouteForge is a **functional beta** release with production-like workflows for read-only validation and demo usage. Current release target: **v0.6.2-beta**.
+RouteForge is a **functional beta** release with production-like workflows for read-only validation and demo usage. Current release target: **v0.6.3-beta**.
## Quickstart with Docker Compose
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 31f748e..49e1113 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -191,3 +191,16 @@ Key capabilities in v0.2.x:
- Export & sharing (Summary, Markdown, HTML)
- ASN-RPKI batch availability explanations
- Demo mode and CI-backed test workflows
+
+## v0.6.3-beta
+
+**Migration UX & Auth Visibility Hotfix**
+
+### Highlights
+
+- Improved visibility for database migration status.
+- Added clearer troubleshooting for missing `created_by_user_id` columns after v0.6 upgrade.
+- Added logged-in user and role display in the UI.
+- Added visible logout action.
+- Improved migration warnings in Dashboard/System views.
+- Improved handling of stale database schemas.
diff --git a/backend/app/api/routes_checks.py b/backend/app/api/routes_checks.py
index 694df19..b45faae 100644
--- a/backend/app/api/routes_checks.py
+++ b/backend/app/api/routes_checks.py
@@ -1,4 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException
+from sqlalchemy.exc import OperationalError
from sqlalchemy.orm import Session
from app.core.auth import require_operator_or_admin
@@ -60,13 +61,19 @@ def check_preflight(payload: PreflightCheckRequest, db: Session = Depends(get_db
def _store_and_respond(db: Session, ctype: str, resource: str, origin_as: str | None, result: dict, user_id: int | None = None) -> CheckResponse:
- check = Check(check_type=ctype, input_resource=resource, origin_as=origin_as, status=result["status"], summary=result["summary"], created_by_user_id=user_id)
- db.add(check)
- db.commit()
- db.refresh(check)
- report_json, md, html = render_report({"check_id": check.id, "input_check_type": ctype, **result})
- report = Report(check_id=check.id, created_by_user_id=user_id, json_data=report_json, markdown=md, html=html)
- db.add(report)
- db.commit()
- db.refresh(report)
- return CheckResponse(report_id=report.id, status=result["status"], summary=result["summary"], explanation=result.get("explanation"), risk=result.get("risk"), recommendations=result["recommendations"], input=result.get("input"), checks=result.get("checks"), details=result["details"], markdown=md, html=html)
+ try:
+ check = Check(check_type=ctype, input_resource=resource, origin_as=origin_as, status=result["status"], summary=result["summary"], created_by_user_id=user_id)
+ db.add(check)
+ db.commit()
+ db.refresh(check)
+ report_json, md, html = render_report({"check_id": check.id, "input_check_type": ctype, **result})
+ report = Report(check_id=check.id, created_by_user_id=user_id, json_data=report_json, markdown=md, html=html)
+ db.add(report)
+ db.commit()
+ db.refresh(report)
+ return CheckResponse(report_id=report.id, status=result["status"], summary=result["summary"], explanation=result.get("explanation"), risk=result.get("risk"), recommendations=result["recommendations"], input=result.get("input"), checks=result.get("checks"), details=result["details"], markdown=md, html=html)
+ except OperationalError as exc:
+ db.rollback()
+ if "no column named created_by_user_id" in str(exc).lower():
+ raise HTTPException(status_code=503, detail="Database schema is not up to date. Please run migrations: docker compose exec backend alembic upgrade head") from exc
+ raise
diff --git a/backend/app/api/routes_system.py b/backend/app/api/routes_system.py
index dc471bd..cc4e7f3 100644
--- a/backend/app/api/routes_system.py
+++ b/backend/app/api/routes_system.py
@@ -12,7 +12,7 @@
def system_info():
return {
'name': 'RouteForge',
- 'version': 'v0.6.2-beta',
+ 'version': 'v0.6.3-beta',
'demo_mode': settings.demo_mode,
'read_only': True,
'data_sources': ['RIPEstat', 'RIPEstat Whois/Registry'],
diff --git a/backend/app/core/system_status.py b/backend/app/core/system_status.py
index 6dd837e..9d03244 100644
--- a/backend/app/core/system_status.py
+++ b/backend/app/core/system_status.py
@@ -6,7 +6,7 @@
from alembic.config import Config
from alembic.runtime.migration import MigrationContext
from alembic.script import ScriptDirectory
-from sqlalchemy import text
+from sqlalchemy import inspect, text
from sqlalchemy.engine import Engine
from app.config import settings
@@ -73,8 +73,23 @@ def _migration_snapshot(engine: Engine) -> dict:
payload["migration_status"] = "up_to_date"
else:
payload["migration_status"] = "behind"
- except Exception:
+ except Exception as exc:
payload["migration_status"] = "unknown"
+ payload["migration_message"] = _safe_error_message(exc)
+ try:
+ with engine.connect() as connection:
+ inspector = inspect(connection)
+ tables = inspector.get_table_names()
+ has_alembic_version = "alembic_version" in tables
+ if not has_alembic_version and tables:
+ payload["schema_version"] = "unknown"
+ if payload["migration_head"] != "unknown":
+ payload["migration_status"] = "behind"
+ payload["migration_message"] = "alembic_version table is missing while database tables exist"
+ else:
+ payload["migration_status"] = "unknown"
+ except Exception:
+ pass
return payload
@@ -114,14 +129,21 @@ def _security_warnings() -> list[str]:
def build_system_status(engine: Engine | None) -> dict:
+ database = get_database_status(engine)
+ operational_warnings: list[str] = []
+ if database.get("migration_status") != "up_to_date":
+ operational_warnings.append(
+ "Database schema is behind the application version. Run database migrations before using checks."
+ )
+
return {
"status": "ok",
"name": settings.app_name,
- "version": "v0.6.2-beta",
+ "version": "v0.6.3-beta",
"read_only": True,
"mode": "demo" if settings.demo_mode else "live",
"demo_mode": settings.demo_mode,
- "database": get_database_status(engine),
+ "database": database,
"api_proxy": {"status": "ok", "mode": "same-origin", "frontend_proxy_expected": True},
"ripestat": {
"cache_ttl_seconds": settings.cache_ttl_seconds,
@@ -131,6 +153,7 @@ def build_system_status(engine: Engine | None) -> dict:
"use_stale_cache_on_error": settings.ripestat_use_stale_cache_on_error,
},
"security_warnings": _security_warnings(),
+ "operational_warnings": operational_warnings,
"features": {
"asn_check": True,
"prefix_check": True,
diff --git a/backend/app/main.py b/backend/app/main.py
index 55cbbbc..86167e0 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -17,7 +17,7 @@
logging.basicConfig(level=getattr(logging, settings.log_level.upper(), logging.INFO))
logger = logging.getLogger("routeforge")
-app = FastAPI(title="RouteForge", version="0.6.2")
+app = FastAPI(title="RouteForge", version="0.6.3")
app.add_middleware(
CORSMiddleware,
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index e055520..ac11b34 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "routeforge-backend"
-version = "0.6.2"
+version = "0.6.3"
description = "RouteForge backend"
license = "AGPL-3.0-or-later"
requires-python = ">=3.12"
diff --git a/backend/tests/test_api_smoke.py b/backend/tests/test_api_smoke.py
index ea5f301..ac45309 100644
--- a/backend/tests/test_api_smoke.py
+++ b/backend/tests/test_api_smoke.py
@@ -177,7 +177,7 @@ def test_system_status_endpoint() -> None:
response = client.get('/api/system/status')
assert response.status_code == 200
payload = response.json()
- assert payload.get('version') == 'v0.6.2-beta'
+ assert payload.get('version') == 'v0.6.3-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
@@ -247,3 +247,34 @@ def test_asn_check_allowed_for_operator() -> None:
assert client.post('/api/auth/login', json={'username': 'operator1', 'password': 'OperatorPass123!'}).status_code == 200
response = client.post('/api/check/asn', json={'asn': 'AS3320'})
assert response.status_code == 200
+
+
+def test_system_status_warns_when_migration_behind(monkeypatch) -> None:
+ from app.core import system_status as ss
+
+ monkeypatch.setattr(ss, "get_database_status", lambda _engine: {"status": "ok", "migration_status": "behind", "schema_version": "0001", "migration_head": "0002"})
+ payload = ss.build_system_status(None)
+ assert payload.get("database", {}).get("migration_status") == "behind"
+ assert payload.get("operational_warnings")
+
+
+def test_check_store_operational_error_message() -> None:
+ from sqlalchemy.exc import OperationalError
+ from app.api import routes_checks as rc
+
+ class FakeDB:
+ def add(self, _):
+ return None
+ def commit(self):
+ raise OperationalError("insert", {}, Exception("no column named created_by_user_id"))
+ def refresh(self, _):
+ return None
+ def rollback(self):
+ return None
+
+ try:
+ rc._store_and_respond(FakeDB(), "asn", "AS3320", None, {"status": "OK", "summary": "ok", "recommendations": [], "details": {}}, 1)
+ assert False
+ except Exception as exc:
+ assert getattr(exc, "status_code", None) == 503
+ assert "Database schema is not up to date" in str(getattr(exc, "detail", ""))
diff --git a/docs/operations/database-migrations.md b/docs/operations/database-migrations.md
index a653d48..cf4173e 100644
--- a/docs/operations/database-migrations.md
+++ b/docs/operations/database-migrations.md
@@ -28,3 +28,34 @@ alembic stamp 0001_initial_schema
## Auto migration
`ROUTEFORGE_AUTO_MIGRATE` is not enabled by default in this beta.
Run migrations manually before production upgrades.
+
+## Troubleshooting: missing `created_by_user_id` after v0.6 upgrade
+
+Fehlerbild:
+
+`sqlite3.OperationalError: table checks has no column named created_by_user_id`
+
+Ursache:
+
+Die Migration `0002_users_and_report_ownership` wurde auf einer bestehenden Datenbank nicht ausgeführt.
+
+Fix:
+
+```bash
+docker compose exec backend alembic current
+docker compose exec backend alembic heads
+docker compose exec backend alembic upgrade head
+```
+
+Wenn bestehende Pre-Alembic DB:
+
+```bash
+docker compose exec backend alembic stamp 0001_initial_schema
+docker compose exec backend alembic upgrade head
+```
+
+Vorher Backup:
+
+```bash
+docker compose exec backend sh -c 'cp /app/data/routeforge.db /app/data/routeforge.db.bak.$(date +%s)'
+```
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 5679618..ae2f0ab 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "routeforge-frontend",
- "version": "0.6.2",
+ "version": "0.6.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "routeforge-frontend",
- "version": "0.6.2",
+ "version": "0.6.3",
"license": "AGPL-3.0-or-later",
"dependencies": {
"react": "^18.3.1",
diff --git a/frontend/package.json b/frontend/package.json
index b86f6a8..5bbbad7 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "routeforge-frontend",
- "version": "0.6.2",
+ "version": "0.6.3",
"private": true,
"license": "AGPL-3.0-or-later",
"type": "module",
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 07e7bce..7d5a4f9 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
-import { ApiError, getMe, getReportHtml, getReportMarkdown, getReportSummary, getReports, getSetupRequired, getSystemInfo, getSystemStatus, login, setupAdmin } from './api'
+import { ApiError, getMe, getReportHtml, getReportMarkdown, getReportSummary, getReports, getSetupRequired, getSystemInfo, getSystemStatus, login, logout, setupAdmin } from './api'
import { AsnCheckForm } from './components/AsnCheckForm'
import { Layout } from './components/Layout'
import { LoginView } from './components/LoginView'
@@ -20,17 +20,14 @@ export default function App() {
const [system, setSystem] = useState
RouteForge v0.6.2-beta
RouteForge v0.6.3-beta
Reports
{reports.length===0 ? {reports.map(r=>
)}{r.summary}