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-v1.0.0-blue" alt="Version">
<img src="https://img.shields.io/badge/version-v1.0.1-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-stable-brightgreen" alt="Status">
<img src="https://img.shields.io/badge/selfhosted-ready-success" alt="Selfhosted">
Expand Down Expand Up @@ -61,7 +61,7 @@ Using read-only checks before execution helps teams validate assumptions without

## Current Release

RouteForge is a **stable** self-hosted release for production-grade read-only validation workflows. Current release: **v1.0.0**.
RouteForge is a **stable** self-hosted release for production-grade read-only validation workflows. Current release: **v1.0.1**.

## Quickstart with Docker Compose

Expand Down
8 changes: 8 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## v1.0.1 (2026-05-22)

- Polished the global layout and removed redundant runtime badges from the live header.
- Improved sidebar branding with a cleaner logo presentation.
- Improved User Management readability with formatted role labels and status badges.
- Improved Audit Log readability with cleaner filters, compact details and a more structured table.
- Kept the release read-only and without database migrations.

## v1.0.0 (2026-05-22)

### Summary
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# RouteForge Roadmap

## Current Status
RouteForge is now **v1.0.0 stable**: a selfhosted, read-only routing preflight release for production-oriented validation workflows.
RouteForge is now **v1.0.1 stable**: a selfhosted, read-only routing preflight release for production-oriented validation workflows.

## v1.0.0 Completed Scope
The stable release includes:
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': 'v1.0.0',
'version': 'v1.0.1',
'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 @@ -153,7 +153,7 @@ def build_system_status(engine: Engine | None) -> dict:
return {
"status": "ok",
"name": settings.app_name,
"version": "v1.0.0",
"version": "v1.0.1",
"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 @@ -20,7 +20,7 @@
logging.basicConfig(level=getattr(logging, settings.log_level.upper(), logging.INFO))
logger = logging.getLogger("routeforge")

app = FastAPI(title="RouteForge", version="1.0.0")
app = FastAPI(title="RouteForge", version="1.0.1")

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 = "1.0.0"
version = "1.0.1"
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') == 'v1.0.0'
assert payload.get('version') == 'v1.0.1'
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
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": "1.0.0",
"version": "1.0.1",
"private": true,
"license": "AGPL-3.0-or-later",
"type": "module",
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ 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}` : 'RouteForge · v1.0.0'
const systemLine = system ? `${system.name} · ${system.version}` : 'RouteForge · v1.0.1'
const title = { dashboard: 'Dashboard', asn: 'ASN Check', prefix: 'Prefix Check', preflight: 'Preflight', 'roa-planner': 'ROA Planner', 'bgp-visibility': 'BGP Visibility', reports: 'Reports', 'watch-mode': 'Watch Mode', 'change-cases': 'Change Cases', system: 'System', users: 'Users', audit: 'Audit Log', about: 'About' }[active]
const proxyStatus = systemStatusError ? 'ERROR' : 'OK'
const migrationStatus = systemStatus?.database?.migration_status || 'unknown'
Expand All @@ -88,8 +88,8 @@ export default function App() {
}
const readOnlyLabel = system?.read_only ? 'Enabled' : 'Disabled'

return <Layout active={active} onNav={setActive} systemLine={systemLine} title={title} demoMode={Boolean(system?.demo_mode)} currentUser={currentUser} onLogout={handleLogout} appVersion={system?.version}>
{active === 'dashboard' && <section className='space-y-4'><article className='rf-card p-6 space-y-2'><h1 className='text-2xl font-bold'>{system?.name || 'RouteForge'} {system?.version || 'v1.0.0'}</h1><div className='grid gap-1 text-sm md:grid-cols-2'><p><b>Current user:</b> {currentUser?.username || '-'}</p><p><b>Role:</b> {currentUser?.role || '-'}</p><p><b>Read-only:</b> {readOnlyLabel}</p><p><b>Mode:</b> {system?.demo_mode ? 'DEMO' : 'LIVE'}</p></div>{system?.demo_mode && <p className='rf-alert border-amber-200 bg-amber-50 text-amber-800'>Demo mode is active.</p>}</article><article className='rf-card p-4'><h3 className='mb-2 text-sm font-semibold'>Quick Actions</h3><div className='flex flex-wrap gap-2'><button className='rf-btn-secondary' onClick={() => setActive('preflight')}>Preflight</button><button className='rf-btn-secondary' onClick={() => setActive('reports')}>Reports</button>{role === 'admin' && <button className='rf-btn-secondary' onClick={() => setActive('system')}>System</button>}</div></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. Run: <code>alembic current</code>, <code>alembic heads</code>, <code>alembic upgrade head</code>.</article>}</section>}
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 space-y-2'><h1 className='text-2xl font-bold'>{system?.name || 'RouteForge'} {system?.version || 'v1.0.1'}</h1><div className='grid gap-1 text-sm md:grid-cols-2'><p><b>Current user:</b> {currentUser?.username || '-'}</p><p><b>Role:</b> {currentUser?.role || '-'}</p><p><b>Read-only:</b> {readOnlyLabel}</p><p><b>Mode:</b> {system?.demo_mode ? 'DEMO' : 'LIVE'}</p></div>{system?.demo_mode && <p className='rf-alert border-amber-200 bg-amber-50 text-amber-800'>Demo mode is active.</p>}</article><article className='rf-card p-4'><h3 className='mb-2 text-sm font-semibold'>Quick Actions</h3><div className='flex flex-wrap gap-2'><button className='rf-btn-secondary' onClick={() => setActive('preflight')}>Preflight</button><button className='rf-btn-secondary' onClick={() => setActive('reports')}>Reports</button>{role === 'admin' && <button className='rf-btn-secondary' onClick={() => setActive('system')}>System</button>}</div></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. Run: <code>alembic current</code>, <code>alembic heads</code>, <code>alembic upgrade head</code>.</article>}</section>}
{!canAccess(active) && <article className='rf-card p-4 text-amber-800 bg-amber-50 border border-amber-200'>You do not have permission to access this section.</article>}
{active === 'asn' && canAccess('asn') && <AsnCheckForm />}
{active === 'prefix' && canAccess('prefix') && <PrefixCheckForm />}
Expand All @@ -102,6 +102,6 @@ export default function App() {
{active === 'system' && canAccess('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. Run: <code>alembic current</code>, <code>alembic heads</code>, <code>alembic upgrade head</code>.</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><div>DB Current Revision: <b>{systemStatus.database?.schema_version || 'unknown'}</b></div><div>DB Head Revision: <b>{systemStatus.database?.migration_head || 'unknown'}</b></div></article>}</section>}
{active === 'users' && canAccess('users') && <UsersView />}
{active === 'audit' && canAccess('audit') && <AuditLogView />}
{active === 'about' && <section className='rf-card p-5 space-y-2 text-sm text-slate-700'><p>RouteForge is a read-only routing operations console for validation workflows.</p><p><b>Read-only:</b> All checks are non-destructive.</p><p><b>Version:</b> {system?.version || 'v1.0.0'}</p><p className='text-xs text-slate-500'>Build and maintained by Norman Sens.</p></section>}
{active === 'about' && <section className='rf-card p-5 space-y-2 text-sm text-slate-700'><p>RouteForge is a read-only routing operations console for validation workflows.</p><p><b>Read-only:</b> All checks are non-destructive.</p><p><b>Version:</b> {system?.version || 'v1.0.1'}</p><p className='text-xs text-slate-500'>Build and maintained by Norman Sens.</p></section>}
</Layout>
}
10 changes: 6 additions & 4 deletions frontend/src/components/AuditLogView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export function AuditLogView() {
const [error, setError] = useState('')
const [action, setAction] = useState('')
const [targetType, setTargetType] = useState('')
const hasFilters = action.trim().length > 0 || targetType.trim().length > 0

useEffect(() => {
const run = async () => {
Expand All @@ -24,13 +25,14 @@ export function AuditLogView() {

return <section className='rf-card p-4 space-y-3'>
<h2 className='text-xl font-semibold'>Audit Log</h2>
<div className='flex gap-2'>
<input className='rf-input' placeholder='Filter action' value={action} onChange={(e)=>setAction(e.target.value)} />
<input className='rf-input' placeholder='Filter target type' value={targetType} onChange={(e)=>setTargetType(e.target.value)} />
<div className='flex flex-wrap items-center gap-2 rounded-xl border border-slate-200 bg-slate-50 p-2'>
<input className='rf-input w-48' placeholder='Filter action' value={action} onChange={(e)=>setAction(e.target.value)} />
<input className='rf-input w-56' placeholder='Filter target type' value={targetType} onChange={(e)=>setTargetType(e.target.value)} />
<button className='rf-btn-secondary' onClick={() => { setAction(''); setTargetType('') }} disabled={!hasFilters}>Clear Filters</button>
</div>
{loading && <div className='text-sm text-slate-500'>Loading audit log…</div>}
{error && <div className='text-sm text-rose-700'>{error}</div>}
{!loading && !error && items.length === 0 && <div className='rounded-xl border border-dashed border-slate-300 p-6 text-sm text-slate-500'>No audit entries found.</div>}
{!loading && !error && items.length > 0 && <div className='overflow-x-auto'><table className='w-full text-xs'><thead><tr><th>Time</th><th>User</th><th>Action</th><th>Target</th><th>IP</th><th>User Agent</th><th>Details</th></tr></thead><tbody>{items.map((a)=><tr key={a.id}><td>{a.created_at}</td><td>{a.username || a.user_id || '-'}</td><td>{a.action}</td><td>{a.target_type || '-'}:{a.target_id || '-'}</td><td>{a.ip_address || '-'}</td><td>{a.user_agent || '-'}</td><td><pre className='whitespace-pre-wrap'>{JSON.stringify(a.details_json || {}, null, 2)}</pre></td></tr>)}</tbody></table></div>}
{!loading && !error && items.length > 0 && <div className='overflow-x-auto'><table className='w-full text-xs'><thead className='text-left text-slate-600'><tr><th className='pb-2'>Time</th><th className='pb-2'>User</th><th className='pb-2'>Action</th><th className='pb-2'>Target</th><th className='pb-2'>IP</th><th className='pb-2'>Details</th></tr></thead><tbody>{items.map((a) => { const target = a.target_type && a.target_id ? `${a.target_type} · ${a.target_id}` : a.target_type || a.target_id || '-'; const detailsPayload = { ...(a.details_json || {}), ...(a.user_agent ? { user_agent: a.user_agent } : {}) }; return <tr key={a.id} className='border-t align-top hover:bg-slate-50'><td className='py-2 pr-3'>{a.created_at}</td><td className='py-2 pr-3'>{a.username || a.user_id || '-'}</td><td className='py-2 pr-3'><span className='rounded-full border border-slate-300 bg-slate-100 px-2 py-1 font-mono text-[11px] text-slate-700'>{a.action}</span></td><td className='py-2 pr-3'>{target}</td><td className='py-2 pr-3'>{a.ip_address || '-'}</td><td className='py-2'><details><summary className='cursor-pointer text-blue-700 hover:text-blue-900'>View</summary><pre className='mt-2 max-w-md whitespace-pre-wrap rounded-lg bg-slate-50 p-2 text-xs text-slate-600'>{JSON.stringify(detailsPayload, null, 2)}</pre></details></td></tr>})}</tbody></table></div>}
</section>
}
15 changes: 6 additions & 9 deletions frontend/src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const nav: { key: NavKey; label: string; desc: string }[] = [
{ key: 'about', label: 'About', desc: 'Data sources and limits' },
]

export function Layout({ children, active, onNav, systemLine, title, demoMode, currentUser, onLogout, appVersion }: { children: ReactNode; active: NavKey; onNav: (key: NavKey) => void; systemLine: string; title: string; demoMode: boolean; currentUser?: { username: string; role: UserRole } | null; onLogout: () => void; appVersion?: string }) {
export function Layout({ children, active, onNav, systemLine, title, demoMode, currentUser, onLogout }: { children: ReactNode; active: NavKey; onNav: (key: NavKey) => void; systemLine: string; title: string; demoMode: boolean; currentUser?: { username: string; role: UserRole } | null; onLogout: () => void }) {
const visibleNav = nav.filter((item) => {
if (!currentUser) return ['dashboard', 'about'].includes(item.key)
if (currentUser.role === 'admin') return true
Expand All @@ -29,12 +29,11 @@ export function Layout({ children, active, onNav, systemLine, title, demoMode, c
return <div className='min-h-screen bg-slate-100 text-slate-900'>
<div className='mx-auto flex max-w-7xl flex-col gap-4 p-4 lg:grid lg:grid-cols-[260px_1fr]'>
<aside className='rf-card p-4'>
<div className='mb-4 flex items-center gap-3'>
<img src='/routeforge.png' alt='RouteForge' className='h-11 w-11 rounded-xl object-contain' />
<div>
<p className='text-xs font-semibold uppercase tracking-wider text-blue-700'>RouteForge</p>
<h1 className='text-xl font-bold'>Operator Console</h1>
<div className='mb-5 flex flex-col items-center gap-3'>
<div className='rounded-2xl bg-slate-900 p-3 shadow-sm'>
<img src='/routeforge.png' alt='RouteForge' className='h-20 w-32 object-contain' />
</div>
<h1 className='text-center text-xl font-bold'>Operator Console</h1>
</div>
<nav className='flex gap-2 overflow-auto lg:flex-col'>
{visibleNav.map((n) => <button key={n.key} onClick={() => onNav(n.key)} className={`min-w-fit rounded-xl px-3 py-2 text-left text-sm transition lg:w-full ${active === n.key ? 'bg-slate-900 text-white' : 'bg-slate-50 text-slate-700 hover:bg-slate-100'}`}><div className='font-semibold'>{n.label}</div><div className={`text-xs ${active === n.key ? 'text-slate-300' : 'text-slate-500'}`}>{n.desc}</div></button>)}
Expand All @@ -46,9 +45,7 @@ export function Layout({ children, active, onNav, systemLine, title, demoMode, c
<h2 className='text-xl font-semibold'>{title}</h2>
<div className='flex flex-wrap items-center gap-2 text-xs font-semibold'>
{currentUser && <span className='rounded-full border border-violet-200 bg-violet-50 px-3 py-1 text-violet-700'>Signed in as {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'>{appVersion || 'v1.0.0'}</span>
{demoMode && <span className='rounded-full border border-amber-300 bg-amber-50 px-3 py-1 text-amber-700'>DEMO</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
Loading