RouteForge v0.8.1-beta
Modernes read-only Operator-Tool für Preflight Checks von ASN, Prefix, RPKI und Registry/IRR.
diff --git a/README.md b/README.md
index 67fd003..fc33d7a 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
- 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.
+
@@ -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.8.1-beta**.
+RouteForge is a **functional beta** release with production-like workflows for read-only validation and demo usage. Current release target: **v0.9.1-rc**.
## Quickstart with Docker Compose
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 8fdbfbe..f434aac 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,3 +1,30 @@
+## v0.9.1-rc (2026-05-20)
+
+### Motivation
+Finalize release-candidate validation for deployment, upgrade discipline, security posture, and role-based UX quality before v1.0.
+
+### Implemented Changes
+- Added deployment health check script (`backend/scripts/check_deployment_health.py`) for API/system/database/build smoke validation.
+- Expanded operations docs with explicit release QA, upgrade QA, Docker QA, and security checklist guidance.
+- Updated release checklist with role/feature UX validation scenarios for admin/operator/viewer.
+- Version bump across backend/frontend/docs to `0.9.1` / `v0.9.1-rc`.
+
+### Deployment QA Notes
+- Use `python backend/scripts/check_deployment_health.py --base-url http://localhost:8000 --check-setup`.
+- Ensure `/api/system/status` reports `read_only=true` and `migration_status` not `behind`.
+
+### Security QA Notes
+- Verify `COOKIE_SECURE=true` for HTTPS.
+- Use explicit `CORS_ORIGINS`, never `*` in production.
+- Ensure Alembic current/head parity before go-live.
+
+### Testing
+- `cd backend && pytest -q`
+- `cd frontend && npm run build`
+
+### Known Limitations
+- Deployment smoke script checks endpoint health/state only; it does not perform synthetic business transactions.
+
## v0.9.0-rc (2026-05-20)
diff --git a/ROADMAP.md b/ROADMAP.md
index ada4a67..0f232be 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -1,22 +1,22 @@
# RouteForge Roadmap
## Current Status
-v0.9.0-rc, BGP Visibility Details completed, read-only
+v0.9.1-rc, BGP Visibility Details completed, read-only
-## v0.9.0-rc
+## v0.9.1-rc
- projects/change cases
- grouped preflight reports
-## v0.9.0-rc
+## v0.9.1-rc
- bgp visibility details
-## v0.9.0-rc
+## v0.9.1-rc
- roa planner / roa preflight
-## v0.9.0-rc
+## v0.9.1-rc
- watch mode / scheduled rechecks
-## v0.9.0-rc
+## v0.9.1-rc
- security review
- UX review
- API stability
diff --git a/backend/app/api/routes_health.py b/backend/app/api/routes_health.py
index a6ce757..1d16f84 100644
--- a/backend/app/api/routes_health.py
+++ b/backend/app/api/routes_health.py
@@ -8,4 +8,4 @@
@router.get('/health')
def health() -> dict:
- return {"status": "ok", "version": "v0.9.0-rc", "database": get_database_status(engine).get("status", "unknown")}
+ return {"status": "ok", "version": "v0.9.1-rc", "database": get_database_status(engine).get("status", "unknown")}
diff --git a/backend/app/api/routes_system.py b/backend/app/api/routes_system.py
index c169116..05d7d98 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.9.0-rc',
+ 'version': 'v0.9.1-rc',
'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 37e1b39..574a784 100644
--- a/backend/app/core/system_status.py
+++ b/backend/app/core/system_status.py
@@ -153,7 +153,7 @@ def build_system_status(engine: Engine | None) -> dict:
return {
"status": "ok",
"name": settings.app_name,
- "version": "v0.9.0-rc",
+ "version": "v0.9.1-rc",
"read_only": True,
"mode": "demo" if settings.demo_mode else "live",
"demo_mode": settings.demo_mode,
diff --git a/backend/app/main.py b/backend/app/main.py
index fa77fc9..f778e90 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -20,7 +20,7 @@
logging.basicConfig(level=getattr(logging, settings.log_level.upper(), logging.INFO))
logger = logging.getLogger("routeforge")
-app = FastAPI(title="RouteForge", version="0.9.0")
+app = FastAPI(title="RouteForge", version="0.9.1")
app.add_middleware(
CORSMiddleware,
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index 6e6da93..eabae7a 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "routeforge-backend"
-version = "0.9.0"
+version = "0.9.1"
description = "RouteForge backend"
license = "AGPL-3.0-or-later"
requires-python = ">=3.12"
diff --git a/backend/scripts/check_deployment_health.py b/backend/scripts/check_deployment_health.py
new file mode 100755
index 0000000..2dd1400
--- /dev/null
+++ b/backend/scripts/check_deployment_health.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python3
+"""RouteForge deployment smoke/health check for release validation."""
+
+from __future__ import annotations
+
+import argparse
+import json
+import sys
+from pathlib import Path
+from urllib.error import HTTPError, URLError
+from urllib.request import urlopen
+
+
+def fetch_json(url: str, timeout: float) -> dict:
+ try:
+ with urlopen(url, timeout=timeout) as resp:
+ return json.loads(resp.read().decode("utf-8"))
+ except HTTPError as exc:
+ raise RuntimeError(f"HTTP {exc.code} for {url}") from exc
+ except URLError as exc:
+ raise RuntimeError(f"Connection error for {url}: {exc.reason}") from exc
+
+
+def check(condition: bool, message: str, failures: list[str]) -> None:
+ prefix = "[OK]" if condition else "[FAIL]"
+ print(f"{prefix} {message}")
+ if not condition:
+ failures.append(message)
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--base-url", default="http://localhost:8000", help="Backend base URL")
+ parser.add_argument("--frontend-dist", default="frontend/dist", help="Frontend build artifact directory")
+ parser.add_argument("--timeout", type=float, default=5.0)
+ parser.add_argument("--check-setup", action="store_true", help="Also validate /api/setup/required endpoint")
+ args = parser.parse_args()
+
+ base = args.base_url.rstrip("/")
+ failures: list[str] = []
+
+ try:
+ health = fetch_json(f"{base}/health", timeout=args.timeout)
+ except RuntimeError as exc:
+ check(False, str(exc), failures)
+ print(f"\nDeployment check failed with {len(failures)} issue(s).")
+ return 1
+ check(health.get("status") == "ok", "Backend /health is reachable and status=ok", failures)
+
+ try:
+ status = fetch_json(f"{base}/api/system/status", timeout=args.timeout)
+ except RuntimeError as exc:
+ check(False, str(exc), failures)
+ print(f"\nDeployment check failed with {len(failures)} issue(s).")
+ return 1
+ check(status.get("status") == "ok", "System status endpoint reachable", failures)
+ check(bool(status.get("version")), "System status contains version", failures)
+ check(status.get("read_only") is True, "System status read_only=true", failures)
+
+ database = status.get("database") if isinstance(status, dict) else None
+ check(isinstance(database, dict), "System status contains database payload", failures)
+ if isinstance(database, dict):
+ check(bool(database.get("status")), "Database status is present", failures)
+ check(bool(database.get("schema_version")), "Database schema_version is present", failures)
+ check(bool(database.get("migration_head")), "Database migration_head is present", failures)
+ check(database.get("migration_status") != "behind", "Migration status is not behind", failures)
+
+ dist = Path(args.frontend_dist)
+ check(dist.exists(), f"Frontend artifacts directory exists ({dist})", failures)
+ check((dist / "index.html").exists(), f"Frontend build artifact exists ({dist / 'index.html'})", failures)
+
+ if args.check_setup:
+ try:
+ setup = fetch_json(f"{base}/api/setup/required", timeout=args.timeout)
+ check("required" in setup, "Setup endpoint reachable and contains required field", failures)
+ except RuntimeError as exc:
+ check(False, str(exc), failures)
+
+ if failures:
+ print(f"\nDeployment check failed with {len(failures)} issue(s).")
+ return 1
+ print("\nDeployment check completed successfully.")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/backend/tests/test_api_smoke.py b/backend/tests/test_api_smoke.py
index a1a4b6e..8a7f78b 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.9.0-rc'
+ assert payload.get('version') == 'v0.9.1-rc'
assert payload.get('read_only') is True
assert payload.get('database', {}).get('status')
assert payload.get('ripestat', {}).get('cache_ttl_seconds') is not None
diff --git a/docs/operations/database-migrations.md b/docs/operations/database-migrations.md
index b6e7a53..56610b3 100644
--- a/docs/operations/database-migrations.md
+++ b/docs/operations/database-migrations.md
@@ -2,15 +2,6 @@
RouteForge uses Alembic for schema lifecycle tracking from `v0.5.3-beta`.
-## Why
-- Traceable schema history
-- Safe, repeatable upgrades
-- Visible migration state in system status
-
-## Engines
-- Production: PostgreSQL (recommended)
-- Dev/demo: SQLite supported
-
## Commands
```bash
docker compose exec backend alembic current
@@ -18,44 +9,30 @@ docker compose exec backend alembic heads
docker compose exec backend alembic upgrade head
```
-## Existing pre-baseline databases
-If the schema already exists from historical `create_all`, mark baseline first:
-```bash
-docker compose exec backend alembic stamp 0001_initial_schema
-docker compose exec backend alembic upgrade head
-```
-
-## 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:
+## Docker QA runbook (v0.9.1-rc)
```bash
+docker compose up -d
+docker compose logs backend
docker compose exec backend alembic current
docker compose exec backend alembic heads
docker compose exec backend alembic upgrade head
+docker compose restart backend
```
-Wenn bestehende Pre-Alembic DB:
+Validation goals:
+- backend comes up without migration crash
+- `alembic current` equals `alembic heads`
+- after restart, `/api/system/status` remains `migration_status=up_to_date`
-```bash
-docker compose exec backend alembic stamp 0001_initial_schema
-docker compose exec backend alembic upgrade head
-```
+## Reverse proxy notes
-Vorher Backup:
+Use a reverse proxy for production TLS termination and same-origin `/api` forwarding.
+See `docs/operations/reverse-proxy.md` for deployment examples.
+## Existing pre-baseline databases
+If schema already exists from historical `create_all`, mark baseline first only after schema verification:
```bash
-docker compose exec backend sh -c 'cp /app/data/routeforge.db /app/data/routeforge.db.bak.$(date +%s)'
+docker compose exec backend alembic stamp 0001_initial_schema
+docker compose exec backend alembic upgrade head
```
diff --git a/docs/operations/release-checklist.md b/docs/operations/release-checklist.md
index 43c83bc..4f721dd 100644
--- a/docs/operations/release-checklist.md
+++ b/docs/operations/release-checklist.md
@@ -1,26 +1,58 @@
# Release Checklist
-## Before release
+## v0.9.1-rc: Deployment QA & UX Validation
+
+### 1) Pre-release validation
- `git pull`
- Run backend tests (`cd backend && pytest -q`)
- Run frontend production build (`cd frontend && npm run build`)
- Validate Compose (`docker compose config` and production config)
-- Check system status endpoint (`/api/system/status`)
-- Check migration status visibility (`database.schema_version`, `migration_head`, `migration_status`)
-## Tagging
+### 2) Deployment smoke check
+
+Run after backend/frontend are up and frontend artifacts were built:
+
+```bash
+python backend/scripts/check_deployment_health.py --base-url http://localhost:8000 --check-setup
+```
+
+The script validates:
+- Backend reachable (`/health`)
+- `/api/system/status` reachable
+- `version` present
+- `read_only=true`
+- `database.status`, `schema_version`, `migration_head` present
+- `migration_status` is not `behind`
+- Frontend build artifacts exist (`frontend/dist/index.html`)
+- Optional `/api/setup/required` reachability
+
+### 3) UX QA checklist
+
+- [ ] Setup Flow works and initial admin creation succeeds
+- [ ] Login/Logout works end-to-end
+- [ ] User Management roles behave correctly (admin/operator/viewer)
+- [ ] Audit Log is visible only for admin users
+- [ ] Change Case create/edit/delete works for authorized roles
+- [ ] BGP Visibility check works and stores report
+- [ ] ROA Planner check works and stores report
+- [ ] Watch Target create/edit/run-due works for authorized roles
+- [ ] Report export works for Markdown/HTML/Summary
+- [ ] Viewer can read data but cannot execute checks/watch/change operations
+- [ ] Operator can execute checks/watch/change cases, but cannot access user/audit admin-only features
+
+### 4) Tagging
-- `git tag -a vX.Y.Z[-beta] -m "RouteForge vX.Y.Z[-beta]"`
-- `git push origin vX.Y.Z[-beta]`
+- `git tag -a v0.9.1-rc -m "RouteForge v0.9.1-rc"`
+- `git push origin v0.9.1-rc`
-## GitHub Release
+### 5) GitHub Release
-- Use release title matching the version and sprint scope
-- Enable **prerelease** checkbox for beta versions
-- Include structured release notes with highlights and known limitations
+- Release title aligned with `v0.9.1-rc`
+- Mark as prerelease
+- Include deployment, upgrade, and security QA notes
-## Post-release smoke test
+### 6) Post-release smoke test
- `GET /health`
- `GET /api/system/info`
diff --git a/docs/operations/security.md b/docs/operations/security.md
index 5a454ca..790b16f 100644
--- a/docs/operations/security.md
+++ b/docs/operations/security.md
@@ -1,5 +1,18 @@
# Security Baseline
+## Security QA checklist (v0.9.1-rc)
+
+- [ ] `SECRET_KEY` changed from any default/dev value
+- [ ] `COOKIE_SECURE=true` when running behind HTTPS
+- [ ] `COOKIE_SAMESITE` is plausible (`lax` recommended, `none` only with `COOKIE_SECURE=true`)
+- [ ] `CORS_ORIGINS` is explicit and not `*`
+- [ ] Admin password is strong and unique
+- [ ] Demo mode is disabled for production usage
+- [ ] Backup exists and restore procedure is tested
+- [ ] Alembic is current (`alembic current == alembic heads`)
+- [ ] Watch Mode external cron/triggering is access-restricted and authenticated
+- [ ] No public write-endpoints exist to RIPE/RPKI/router systems
+
## Read-only safety model
RouteForge is read-only by design. It validates routing state and preflight conditions but does not push configuration changes to external systems.
@@ -14,13 +27,12 @@ Use RouteForge behind a reverse proxy with TLS termination in production (for ex
- Never commit `.env` files to git.
- Change `POSTGRES_PASSWORD` from the example default.
-## Default password warning
-
-`/api/system/status` exposes `security_warnings` when a risky default is detected (currently: `POSTGRES_PASSWORD=change-me`). No secret values are returned.
+## HTTPS cookie and CORS guidance
-## CORS guidance
-
-In the standard Docker setup, frontend and API are same-origin via nginx (`/api` proxy). CORS is mainly relevant for split deployments where frontend and backend are served from different origins.
+- `COOKIE_SECURE=true` for HTTPS deployments.
+- `COOKIE_SAMESITE=lax` is the recommended default.
+- `COOKIE_SAMESITE=none` requires `COOKIE_SECURE=true`.
+- In split-origin deployments set explicit `CORS_ORIGINS` to frontend domains.
## Security headers
@@ -32,39 +44,8 @@ Frontend nginx sets baseline headers:
- `Permissions-Policy: geolocation=(), microphone=(), camera=()`
- A conservative initial Content-Security-Policy for SPA usage
-## Container hardening
-
-- Backend container starts through an entrypoint as root only long enough to normalize `/app/data` ownership, then drops privileges to non-root user `routeforge` for runtime.
-
-- Backend container runs as non-root user `routeforge`.
-- Backend image keeps only runtime-relevant files.
-- Frontend image is multi-stage (build + runtime).
-- Nginx container still runs with default upstream behavior; strict non-root nginx runtime can be added later as a dedicated hardening step.
-
-## SQLite volume permissions (Docker Compose)
-
-In the standard `docker-compose.yml` setup with SQLite, the database file is stored at `/app/data/routeforge.db` via the named volume mount `routeforge_data:/app/data`.
-
-The backend entrypoint ensures `/app/data` is writable for the non-root runtime user `routeforge` on container start. This prevents SQLite write failures such as `sqlite3.OperationalError: attempt to write a readonly database` when a mounted volume is root-owned.
-
## What RouteForge does not do
- no ROA creation
- no RIPE DB writes
- no router deployment
-
-## Current limitations
-
-- Role model: admin/operator/viewer
-- Admin-only user management
-- Inactive users cannot log in
-- Password reset is admin-driven (set new password in user management)
-- No external auth/SSO in this version
-
-
-## v0.9.0-rc baseline
-
-- Session cookies use `HttpOnly`, configurable `COOKIE_SAMESITE` (default `lax`) and `COOKIE_SECURE` policy.
-- Password hashing uses `pbkdf2_sha256` with legacy `sha256` verify compatibility for existing users.
-- Viewer role remains strictly read-only for checks and watch execution/mutation endpoints.
-- RouteForge remains read-only to external systems (no write operations to RIPE DB/RPKI/router APIs).
diff --git a/docs/operations/upgrades.md b/docs/operations/upgrades.md
index a01d48d..aea7efc 100644
--- a/docs/operations/upgrades.md
+++ b/docs/operations/upgrades.md
@@ -9,6 +9,62 @@ git pull
docker compose -f docker-compose.prod.yml build
```
+## Upgrade QA matrix (v0.9.1-rc)
+
+### A) Fresh SQLite DB
+
+```bash
+rm -f backend/data/routeforge.db
+cd backend && alembic upgrade head
+cd .. && docker compose up -d
+```
+
+Expected:
+- backend starts cleanly
+- `/api/system/status` shows `migration_status=up_to_date`
+
+### B) Existing SQLite DB from older revision
+
+```bash
+cd backend && alembic current
+cd backend && alembic heads
+cd backend && alembic upgrade head
+cd .. && docker compose restart backend
+```
+
+Expected:
+- `alembic current` matches `alembic heads`
+- application starts without migration warnings
+
+### C) Behavior when DB is behind
+
+Expected behavior:
+- `/api/system/status` returns database migration status `behind`
+- operational warnings include upgrade command hints
+- release/deployment checks must fail until DB is upgraded
+
+### D) `ALLOW_SQLITE_CREATE_ALL=false`
+
+Expected behavior:
+- SQLite startup must not silently create schema via `create_all`
+- migration discipline remains Alembic-first
+
+### E) Safe `alembic stamp` usage
+
+Use `alembic stamp
RouteForge v0.8.1-beta
alembic current, alembic heads, alembic upgrade head.RouteForge v0.9.1-rc
alembic current, alembic heads, alembic upgrade head.