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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<img src="frontend/public/routeforge.png" alt="RouteForge Logo" width="420">
</p>
<p align="center">
<img src="https://img.shields.io/badge/version-v0.6.3--beta-blue" alt="Version">
<img src="https://img.shields.io/badge/version-v0.6.4--beta-blue" alt="Version">
<img src="https://img.shields.io/badge/license-AGPL--3.0--or--later-orange" alt="License">
<img src="https://img.shields.io/badge/status-beta-yellow" alt="Status">
<img src="https://img.shields.io/badge/selfhosted-ready-success" alt="Selfhosted">
Expand Down Expand Up @@ -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.3-beta**.
RouteForge is a **functional beta** release with production-like workflows for read-only validation and demo usage. Current release target: **v0.6.4-beta**.

## Quickstart with Docker Compose

Expand Down
15 changes: 14 additions & 1 deletion RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Release Notes

## v0.6.4-beta

**Alembic Logging Config Hotfix**

### Highlights

- Fixed Alembic CLI crash with `KeyError: "formatters"`.
- Alembic `env.py` now handles minimal `alembic.ini` files safely.
- Database migration commands now work in Docker selfhosting setups.
- Migration troubleshooting documentation updated.

---

## v0.6.2-beta

**Frontend Auth Render Hotfix**
Expand Down Expand Up @@ -192,7 +205,7 @@ Key capabilities in v0.2.x:
- ASN-RPKI batch availability explanations
- Demo mode and CI-backed test workflows

## v0.6.3-beta
## v0.6.4-beta

**Migration UX & Auth Visibility Hotfix**

Expand Down
34 changes: 34 additions & 0 deletions backend/alembic.ini
Original file line number Diff line number Diff line change
@@ -1,3 +1,37 @@
[alembic]
script_location = alembic
sqlalchemy.url = sqlite:///./routeforge.db

[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
18 changes: 16 additions & 2 deletions backend/alembic/env.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import annotations

from logging.config import fileConfig
from pathlib import Path
import configparser

from alembic import context
from sqlalchemy import engine_from_config, pool
Expand All @@ -12,8 +14,20 @@
config = context.config
config.set_main_option("sqlalchemy.url", settings.database_url)

if config.config_file_name is not None:
fileConfig(config.config_file_name)
def _configure_logging() -> None:
if not config.config_file_name:
return
config_path = Path(config.config_file_name)
if not config_path.exists():
return
parser = configparser.ConfigParser()
parser.read(config_path)
required = {"loggers", "handlers", "formatters"}
if required.issubset(set(parser.sections())):
fileConfig(config_path)


_configure_logging()

target_metadata = Base.metadata

Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/routes_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
def system_info():
return {
'name': 'RouteForge',
'version': 'v0.6.3-beta',
'version': 'v0.6.4-beta',
'demo_mode': settings.demo_mode,
'read_only': True,
'data_sources': ['RIPEstat', 'RIPEstat Whois/Registry'],
Expand Down
2 changes: 1 addition & 1 deletion backend/app/core/system_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ def build_system_status(engine: Engine | None) -> dict:
return {
"status": "ok",
"name": settings.app_name,
"version": "v0.6.3-beta",
"version": "v0.6.4-beta",
"read_only": True,
"mode": "demo" if settings.demo_mode else "live",
"demo_mode": settings.demo_mode,
Expand Down
2 changes: 1 addition & 1 deletion backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.3")
app = FastAPI(title="RouteForge", version="0.6.4")

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.6.3"
version = "0.6.4"
description = "RouteForge backend"
license = "AGPL-3.0-or-later"
requires-python = ">=3.12"
Expand Down
2 changes: 1 addition & 1 deletion backend/tests/test_api_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.3-beta'
assert payload.get('version') == 'v0.6.4-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
Expand Down
10 changes: 5 additions & 5 deletions docs/operations/database-migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ RouteForge uses Alembic for schema lifecycle tracking from `v0.5.3-beta`.

## Commands
```bash
cd backend
alembic history
alembic current
alembic upgrade head
docker compose exec backend alembic current
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
alembic stamp 0001_initial_schema
docker compose exec backend alembic stamp 0001_initial_schema
docker compose exec backend alembic upgrade head
```

## Auto migration
Expand Down
15 changes: 15 additions & 0 deletions docs/operations/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,18 @@ docker compose down
docker compose run --rm --user root backend sh -c "mkdir -p /app/data && chown -R routeforge:routeforge /app/data && chmod -R u+rwX /app/data"
docker compose up -d --build
```


## KeyError: 'formatters' (Alembic)

### Ursache

Ältere Versionen hatten eine minimale `backend/alembic.ini` ohne Logging-Sektionen, während `backend/alembic/env.py` `fileConfig(...)` aufgerufen hat.

### Fix

Update auf `v0.6.4-beta` oder neuer, dann:

```bash
docker compose exec backend alembic upgrade head
```
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.6.3",
"version": "0.6.4",
"private": true,
"license": "AGPL-3.0-or-later",
"type": "module",
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,19 @@ export default function App() {
if (authMode === 'login') return <LoginView onSubmit={onLoginSubmit} error={authError} />
if (authMode === 'error') return <div className='p-8 text-center text-rose-700'>{authError}</div>

const systemLine = system ? `${system.name} ${system.version} · mode=${system.demo_mode ? 'DEMO' : 'LIVE'} · read_only=${String(system.read_only)}` : 'RouteForge v0.6.3-beta · read-only preflight checks'
const systemLine = system ? `${system.name} ${system.version} · mode=${system.demo_mode ? 'DEMO' : 'LIVE'} · read_only=${String(system.read_only)}` : 'RouteForge v0.6.4-beta · read-only preflight checks'
const title = { dashboard: 'Dashboard', asn: 'ASN Check', prefix: 'Prefix Check', preflight: 'Preflight Check', reports: 'Reports', system: 'System Status', about: 'About RouteForge' }[active]
const proxyStatus = systemStatusError ? 'ERROR' : 'OK'
const migrationStatus = systemStatus?.database?.migration_status || 'unknown'
const migrationsBlocked = migrationStatus === 'behind' || migrationStatus === 'error'

return <Layout active={active} onNav={setActive} systemLine={systemLine} title={title} demoMode={Boolean(system?.demo_mode)} currentUser={currentUser} onLogout={handleLogout}>
{active === 'dashboard' && <section className='space-y-4'><article className='rf-card p-6'><h1 className='text-2xl font-bold'>RouteForge v0.6.3-beta</h1><p className='mt-2 text-slate-600'>Modernes read-only Operator-Tool für Preflight Checks von ASN, Prefix, RPKI und Registry/IRR.</p></article>{migrationsBlocked && <article className='rf-card border border-amber-300 bg-amber-50 p-4 text-amber-900'>Database migrations are required before using RouteForge.</article>}</section>}
{active === 'dashboard' && <section className='space-y-4'><article className='rf-card p-6'><h1 className='text-2xl font-bold'>RouteForge v0.6.4-beta</h1><p className='mt-2 text-slate-600'>Modernes read-only Operator-Tool für Preflight Checks von ASN, Prefix, RPKI und Registry/IRR.</p></article>{migrationsBlocked && <article className='rf-card border border-amber-300 bg-amber-50 p-4 text-amber-900'>Database migrations are required before using RouteForge.</article>}</section>}
{active === 'asn' && <AsnCheckForm />}
{active === 'prefix' && <PrefixCheckForm />}
{active === 'preflight' && <PreflightCheckForm />}
{active === 'reports' && <section className='rf-card p-4'><h2 className='mb-3 text-xl font-semibold'>Reports</h2>{reports.length===0 ? <div className='rounded-xl border border-dashed border-slate-300 p-6 text-sm text-slate-500'>Noch keine Reports vorhanden.</div> : <div className='overflow-x-auto'><table className='w-full text-sm'><tbody>{reports.map(r=><tr key={r.report_id}><td>{r.summary}</td><td><button className='rf-btn-secondary' onClick={async ()=>navigator.clipboard?.writeText(await getReportSummary(r.report_id))}>Copy Summary</button><button className='rf-btn-secondary' onClick={async ()=>{const t=await getReportMarkdown(r.report_id);const a=document.createElement('a');a.href=URL.createObjectURL(new Blob([t],{type:'text/markdown'}));a.download=`routeforge-report-${r.report_id}.md`;a.click()}}>Download Markdown</button><button className='rf-btn-secondary' onClick={async ()=>{const t=await getReportHtml(r.report_id);const a=document.createElement('a');a.href=URL.createObjectURL(new Blob([t],{type:'text/html'}));a.download=`routeforge-report-${r.report_id}.html`;a.click()}}>Download HTML</button></td></tr>)}</tbody></table></div>}</section>}
{active === 'system' && <section className='space-y-3'>{systemStatusError && <article className='rf-card p-4 text-rose-700'>{systemStatusError}</article>}{migrationsBlocked && <article className='rf-card border border-amber-300 bg-amber-50 p-4 text-amber-900'>Database migrations are required before using RouteForge.</article>}{systemStatus && <article className='rf-card p-4 grid gap-2 md:grid-cols-2 text-sm'><div>Version: <b>{systemStatus.version}</b></div><div>Mode: <b>{systemStatus.mode}</b></div><div>API Proxy: <b>{proxyStatus}</b></div><div>Migration Status: <b>{migrationStatus}</b> <StatusBadge status={migrationStatus === 'up_to_date' ? 'OK' : migrationStatus === 'behind' ? 'WARNING' : migrationStatus === 'error' ? 'CRITICAL' : 'UNKNOWN'} /></div></article>}</section>}
{active === 'about' && <section className='rf-card p-5 space-y-2 text-sm text-slate-700'><p><b>Version:</b> v0.6.3-beta</p></section>}
{active === 'about' && <section className='rf-card p-5 space-y-2 text-sm text-slate-700'><p><b>Version:</b> v0.6.4-beta</p></section>}
</Layout>
}
2 changes: 1 addition & 1 deletion frontend/src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function Layout({ children, active, onNav, systemLine, title, demoMode, c
{currentUser && <span className='rounded-full border border-violet-200 bg-violet-50 px-3 py-1 text-violet-700'>Angemeldet als {currentUser.username} · {currentUser.role}</span>}
<span className={`rounded-full border px-3 py-1 ${demoMode ? 'border-amber-300 bg-amber-50 text-amber-700' : 'border-emerald-300 bg-emerald-50 text-emerald-700'}`}>{demoMode ? 'DEMO' : 'LIVE'}</span>
<span className='rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-blue-700'>READ-ONLY</span>
<span className='rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-slate-700'>v0.6.3-beta</span>
<span className='rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-slate-700'>v0.6.4-beta</span>
<button className='rounded-full border border-rose-200 bg-rose-50 px-3 py-1 text-rose-700 hover:bg-rose-100' onClick={onLogout}>Logout</button>
</div>
</header>
Expand Down
Loading