From 66ae94612b2f758d4c661ade71c9ad12bac8a636 Mon Sep 17 00:00:00 2001 From: DeepZone Date: Fri, 22 May 2026 09:25:11 +0100 Subject: [PATCH] v1.0.0: stable release versioning and docs cleanup --- README.md | 10 ++--- RELEASE_NOTES.md | 23 +++++++++- ROADMAP.md | 43 ++++++++++--------- backend/app/api/routes_system.py | 2 +- backend/app/core/prefix_evaluation.py | 34 +++++++-------- backend/app/core/recommendations.py | 20 ++++----- backend/app/core/system_status.py | 2 +- backend/app/main.py | 2 +- backend/app/services/asn_checker.py | 4 +- .../app/services/bgp_visibility_service.py | 2 +- backend/app/services/registry_checker.py | 38 ++++++++-------- backend/app/services/roa_planner_service.py | 18 ++++---- .../services/routing_visibility_checker.py | 26 +++++------ backend/app/services/rpki_checker.py | 2 +- backend/pyproject.toml | 2 +- backend/tests/test_api_smoke.py | 2 +- docs/operations/release-checklist.md | 10 ++--- docs/operations/security.md | 2 +- docs/operations/troubleshooting.md | 38 ++++++++-------- docs/operations/upgrades.md | 2 +- frontend/package.json | 2 +- frontend/src/App.tsx | 8 ++-- frontend/src/components/Layout.tsx | 4 +- 23 files changed, 159 insertions(+), 137 deletions(-) diff --git a/README.md b/README.md index b11f1e2..44f2d85 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ RouteForge Logo

- Version + Version License - Status + Status Selfhosted Read-only

@@ -41,9 +41,9 @@ It combines these checks into an explainable, read-only preflight workflow. Routing changes often require fast but traceable checks across multiple external data views. RouteForge provides a single UI/API workflow so teams can run consistent preflight checks, share results, and keep a documented decision trail. -## Current Alpha Status +## Current Release -RouteForge is a **functional beta** release with production-like workflows for read-only validation and demo usage. Current release target: **v0.9.2-rc**. +RouteForge is a **stable selfhosted** release for production-grade read-only validation workflows. Current release: **v1.0.0**. ## Quickstart with Docker Compose @@ -174,7 +174,7 @@ RouteForge is read-only by design: ## Known Limitations -- Alpha software, interfaces may evolve. +- Stable release with ongoing minor UX/documentation improvements. - RIPEstat payloads can vary over time. - No local RPKI validator yet. - No full BGP monitoring replacement. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 2565c57..16bdc0a 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,24 @@ +## v1.0.0 (2026-05-22) + +### Summary +- Stable selfhosted RouteForge release. +- Read-only routing safety model kept as a strict product baseline. +- User management and role-based access control. +- Audit Log. +- Change Cases. +- ASN Check, Prefix Check, and Preflight checks. +- BGP Visibility. +- ROA Planner / ROA Preflight. +- Watch Mode and scheduled rechecks. +- Reports and exports. +- System status, security warnings, and upgrade documentation. +- English-only UI and output polish. + +### Upgrade Notes +- Upgrade backend/frontend to `1.0.0` and API-visible version to `v1.0.0`. +- Keep Alembic migration discipline (`alembic current`, `alembic heads`, `alembic upgrade head`) before production use. +- No new database migration was introduced specifically for this stable cut. + ## v0.9.2-rc (2026-05-21) - UI Cleanup & English-only polish @@ -91,8 +112,6 @@ BGP visibility checks remain read-only and do not modify RIPE DB, RPKI objects, - Added audit events for Change Case and attachment operations. - Security: Change Cases are local workflow metadata only; no writes to RIPE DB, RPKI, or routers are performed. -# Release Notes - ## v0.8.1-beta **Stabilization, UX Polish & Upgrade Safety** diff --git a/ROADMAP.md b/ROADMAP.md index 92e1676..c3fc156 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,26 +1,29 @@ # RouteForge Roadmap ## Current Status -v0.9.2-rc, BGP Visibility Details completed, read-only +RouteForge is now **v1.0.0 stable**: a selfhosted, read-only routing preflight release for production-oriented validation workflows. -## v0.9.2-rc -- projects/change cases -- grouped preflight reports +## v1.0.0 Completed Scope +The stable release includes: +- User Management +- Role-based access control +- Session authentication +- Audit Log +- Change Cases +- ASN Check +- Prefix Check +- Preflight +- BGP Visibility +- ROA Planner / ROA Preflight +- Watch Mode / Scheduled Rechecks +- Reports and exports +- System Status +- Security and upgrade documentation +- English-only UI and output cleanup +- Read-only safety model -## v0.9.2-rc -- bgp visibility details +## Historical Release Context +Recent release-candidate work (v0.9.x-rc) focused on stabilizing BGP visibility details, ROA preflight workflows, watch scheduling/rechecks, deployment QA, and upgrade discipline that culminated in v1.0.0. -## v0.9.2-rc -- roa planner / roa preflight - -## v0.9.2-rc -- watch mode / scheduled rechecks - -## v0.9.2-rc -- security review -- UX review -- API stability -- upgrade tests - -## v1.0.0 -- stable selfhosted RouteForge release +## Future Roadmap +Post-v1.0.0 work will focus on operational maturity, documentation quality, and maintainability without changing the core read-only safety model. diff --git a/backend/app/api/routes_system.py b/backend/app/api/routes_system.py index 9cd9344..4bb2e66 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.2-rc', + 'version': 'v1.0.0', 'demo_mode': settings.demo_mode, 'read_only': True, 'data_sources': ['RIPEstat', 'RIPEstat Whois/Registry'], diff --git a/backend/app/core/prefix_evaluation.py b/backend/app/core/prefix_evaluation.py index 8e64ff4..d890eae 100644 --- a/backend/app/core/prefix_evaluation.py +++ b/backend/app/core/prefix_evaluation.py @@ -20,33 +20,33 @@ def evaluate_prefix_overall( if CheckStatus.CRITICAL in statuses.values(): if statuses["routing"] == CheckStatus.CRITICAL: - return _critical("RPKI und Registry wirken plausibel, aber die sichtbare Routing-Origin weicht ab.", "Das Prefix ist sichtbar, aber nicht mit dem erwarteten Origin-AS.") + return _critical("RPKI and registry look plausible, but visible routing origin differs.", "The prefix is visible, but not with the expected origin AS.") if statuses["rpki"] == CheckStatus.CRITICAL: return _critical("Das Prefix ist zwar sichtbar, aber RPKI meldet ein kritisches Problem.", "RPKI bewertet das Prefix-Origin-Paar kritisch, obwohl andere Checks ggf. positive Hinweise liefern.") - return _critical("Registry/IRR-Origin widerspricht dem angegebenen Origin-AS.", "Ein gefundenes route/route6-Origin weicht vom geprüften Origin-AS ab.") + return _critical("Registry/IRR origin conflicts with the provided origin AS.", "A discovered route/route6 origin differs from the checked origin AS.") if statuses["rpki"] == statuses["registry"] == statuses["routing"] == CheckStatus.UNKNOWN: - return _unknown("No reliable overall assessment possible.", "RPKI, Registry/IRR und Routing Visibility liefern keine verlässliche Aussage.") + return _unknown("No reliable overall assessment possible.", "RPKI, Registry/IRR, and routing visibility do not provide a reliable assessment.") if statuses["rpki"] == CheckStatus.OK and statuses["registry"] == CheckStatus.OK and statuses["routing"] == CheckStatus.OK: return { "status": CheckStatus.OK.value, - "summary": "Prefix-Origin-Paar wirkt autorisiert, dokumentiert und sichtbar.", - "explanation": "RPKI, Registry/IRR und Routing Visibility zeigen ein konsistentes Ergebnis.", - "risk": "Derzeit keine offensichtliche Inkonsistenz erkennbar.", - "recommendations": ["Routing-Sichtbarkeit und RPKI/Registry-Daten weiter überwachen."], + "summary": "Prefix-origin pair appears authorized, documented, and visible.", + "explanation": "RPKI, Registry/IRR, and routing visibility show a consistent result.", + "risk": "No obvious inconsistency is currently visible.", + "recommendations": ["Continue monitoring routing visibility and RPKI/registry data."], } if statuses["rpki"] == CheckStatus.OK and statuses["registry"] == CheckStatus.OK and statuses["routing"] == CheckStatus.UNKNOWN: - return _warning("Routing visibility could not be determined reliably.", "RPKI und Registry/IRR sind plausibel, aber die Routing-Sichtbarkeit bleibt unklar.") + return _warning("Routing visibility could not be determined reliably.", "RPKI and Registry/IRR are plausible, but routing visibility remains unclear.") if CheckStatus.WARNING in statuses.values(): - return _warning("Kombinierte Prefix-Bewertung zeigt Warnhinweise.", "Mindestens eine Einzelprüfung meldet unvollständige oder unsichere Daten.") + return _warning("Combined prefix evaluation shows warnings.", "At least one individual check reports incomplete or uncertain data.") if CheckStatus.UNKNOWN in statuses.values(): - return _warning("Teilweise bestätigte Datenlage mit Unsicherheit.", "Mindestens eine Quelle ist unklar; die Gesamtbewertung bleibt daher konservativ WARNING.") + return _warning("Partially confirmed data with uncertainty.", "At least one source is unclear; the overall assessment remains conservatively WARNING.") - return _unknown("Kombinierte Prefix-Bewertung nicht eindeutig bestimmbar.", "Die vorliegenden Einzelergebnisse konnten nicht konsistent kombiniert werden.") + return _unknown("Combined prefix evaluation is not clearly determinable.", "The available individual results could not be combined consistently.") def _warning(summary: str, explanation: str) -> dict: @@ -54,8 +54,8 @@ def _warning(summary: str, explanation: str) -> dict: "status": CheckStatus.WARNING.value, "summary": summary, "explanation": explanation, - "risk": "Die Gesamtaussage bleibt eingeschränkt.", - "recommendations": ["Individual checks und Rohdaten gezielt nacharbeiten."], + "risk": "The overall conclusion remains limited.", + "recommendations": ["Review individual checks and raw data in detail."], } @@ -64,8 +64,8 @@ def _critical(summary: str, explanation: str) -> dict: "status": CheckStatus.CRITICAL.value, "summary": summary, "explanation": explanation, - "risk": "Erhöhtes Risk für Fehlrouting, Erreichbarkeitsprobleme oder Sicherheitsvorfälle.", - "recommendations": ["Abweichung priorisiert prüfen und beheben."], + "risk": "Increased risk of misrouting, reachability issues, or security incidents.", + "recommendations": ["Prioritize checking and resolving the discrepancy."], } @@ -74,8 +74,8 @@ def _unknown(summary: str, explanation: str) -> dict: "status": CheckStatus.UNKNOWN.value, "summary": summary, "explanation": explanation, - "risk": "Die Datenlage ist unzureichend für eine belastbare Routing-Sicherheitsbewertung.", - "recommendations": ["Prüfung später wiederholen und Rohdaten kontrollieren."], + "risk": "The data is insufficient for a reliable routing security assessment.", + "recommendations": ["Repeat the check later and verify raw data."], } diff --git a/backend/app/core/recommendations.py b/backend/app/core/recommendations.py index 2a39b5f..434c250 100644 --- a/backend/app/core/recommendations.py +++ b/backend/app/core/recommendations.py @@ -7,7 +7,7 @@ def default_recommendations(status: CheckStatus) -> list[str]: if status == CheckStatus.WARNING: return ["Review RPKI/registry data and improve coverage."] if status == CheckStatus.CRITICAL: - return ["Origin-AS und ROA sofort verifizieren, da Route verworfen werden kann."] + return ["Verify origin AS and ROA immediately, because the route may be rejected."] return ["Check again; external data source was unreliable or unavailable."] @@ -38,20 +38,20 @@ def evaluate_rpki_status(rpki_status: str | None, prefix: str, origin_as: str | return { "status": CheckStatus.CRITICAL.value, "summary": "RPKI validation failed", - "explanation": "Das Prefix wird mit einem Origin-AS geprüft, das nicht durch einen passenden ROA gedeckt ist.", + "explanation": "The prefix is checked with an origin AS that is not covered by a matching ROA.", "risk": "Validierende Netze können diese Route verwerfen. Dadurch kann Erreichbarkeit verloren gehen.", "recommendations": [ "Check whether the origin AS is correct.", "Review existing ROAs for the prefix.", - "Erstelle oder korrigiere den ROA nur, wenn du zur Verwaltung dieser Ressourcen berechtigt bist.", + "Create or correct the ROA only if you are authorized to manage these resources.", ], } if normalized == "invalid_asn": return { "status": CheckStatus.CRITICAL.value, "summary": "RPKI invalid: unauthorized origin AS", - "explanation": "Für das Prefix existiert ein ROA, aber nicht für dieses Origin-AS.", - "risk": "Validierende Netze können diese Route verwerfen, weil das Origin-AS nicht autorisiert ist.", + "explanation": "A ROA exists for the prefix, but not for this origin AS.", + "risk": "Validating networks may reject this route because the origin AS is not authorized.", "recommendations": [ "Check the origin AS.", "Review the ROA.", @@ -62,8 +62,8 @@ def evaluate_rpki_status(rpki_status: str | None, prefix: str, origin_as: str | return { "status": CheckStatus.CRITICAL.value, "summary": "RPKI invalid: announced prefix too specific", - "explanation": "Für das Prefix existiert ein ROA, aber die angekündigte Prefix-Länge ist länger als die erlaubte maxLength.", - "risk": "Validierende Netze können diese Route verwerfen, obwohl das AS grundsätzlich passen kann.", + "explanation": "A ROA exists for the prefix, but the announced prefix length is longer than the allowed maxLength.", + "risk": "Validating networks may reject this route, even though the AS may otherwise match.", "recommendations": [ "Check announced prefix length.", "Check ROA maxLength.", @@ -74,8 +74,8 @@ def evaluate_rpki_status(rpki_status: str | None, prefix: str, origin_as: str | return { "status": CheckStatus.WARNING.value, "summary": "No matching ROA found", - "explanation": "Für dieses Prefix-Origin-Paar wurde kein passender ROA gefunden.", - "risk": "Das ist nicht automatisch ein Ausfall, schwächt aber die Routing-Sicherheit.", + "explanation": "No matching ROA was found for this prefix-origin pair.", + "risk": "This is not automatically an outage, but it weakens routing security.", "recommendations": [ "Check whether a ROA should be created.", "Nur anlegen, wenn man zur Verwaltung berechtigt ist.", @@ -90,6 +90,6 @@ def evaluate_rpki_status(rpki_status: str | None, prefix: str, origin_as: str | "recommendations": [ "Review the API raw data.", "Repeat the check later.", - "Vergleiche bei Bedarf mit einer zweiten Quelle oder einem lokalen RPKI-Validator.", + "Compare with a secondary source or a local RPKI validator if needed.", ], } diff --git a/backend/app/core/system_status.py b/backend/app/core/system_status.py index 69253cb..0e02aa4 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.2-rc", + "version": "v1.0.0", "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 1be6ba2..1578091 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.2") +app = FastAPI(title="RouteForge", version="1.0.0") app.add_middleware( CORSMiddleware, diff --git a/backend/app/services/asn_checker.py b/backend/app/services/asn_checker.py index f84e6e2..d7bc82e 100644 --- a/backend/app/services/asn_checker.py +++ b/backend/app/services/asn_checker.py @@ -49,9 +49,9 @@ def check(self, asn_input: str) -> dict: errors = [] if "error" in overview: - errors.append("as-overview nicht erreichbar") + errors.append("as-overview not reachable") if "error" in prefixes: - errors.append("announced-prefixes nicht erreichbar") + errors.append("announced-prefixes not reachable") status = CheckStatus.UNKNOWN.value if errors else CheckStatus.OK.value summary = f"ASN {resource} checked." return { diff --git a/backend/app/services/bgp_visibility_service.py b/backend/app/services/bgp_visibility_service.py index 8d8cc74..48b991c 100644 --- a/backend/app/services/bgp_visibility_service.py +++ b/backend/app/services/bgp_visibility_service.py @@ -53,7 +53,7 @@ def check(self, prefix: str, expected_origin_as: str | None) -> dict: return { "status": status, "summary": summary, - "explanation": "BGP Visibility basiert auf RIPEstat-Daten und ist read-only.", + "explanation": "BGP visibility is based on RIPEstat data and is read-only.", "risk": "External visibility data may be delayed or incomplete.", "recommendations": recommendations, "input": {"prefix": normalized_prefix, "expected_origin_as": normalized_expected}, diff --git a/backend/app/services/registry_checker.py b/backend/app/services/registry_checker.py index 72bafd4..2413775 100644 --- a/backend/app/services/registry_checker.py +++ b/backend/app/services/registry_checker.py @@ -7,11 +7,11 @@ def check(self, prefix: str, origin_as: str | None, whois_payload: dict) -> dict if not isinstance(whois_payload, dict) or whois_payload.get("error"): return { "status": CheckStatus.UNKNOWN.value, - "summary": "Registry-/IRR-Daten konnten nicht bestimmt werden", - "explanation": "Die Whois-/Registry-Datenquelle war nicht erreichbar oder lieferte einen Fehler.", + "summary": "Registry/IRR data could not be determined", + "explanation": "The Whois/Registry data source was unreachable or returned an error.", "risk": "The assessment is incomplete.", "recommendations": [ - "Prüfe die Rohdaten der Registry-Quelle.", + "Check raw data from the registry source.", "Retry the query later.", "Vergleiche das Ergebnis mit einer zweiten Registry-/IRR-Quelle.", ], @@ -25,10 +25,10 @@ def check(self, prefix: str, origin_as: str | None, whois_payload: dict) -> dict return { "status": CheckStatus.UNKNOWN.value, "summary": "Keine verwertbaren Registry-/IRR-Daten gefunden", - "explanation": "Die Quelle lieferte keine eindeutig parsebaren Daten zum Prefix.", - "risk": "Es kann keine belastbare Plausibilitätsaussage getroffen werden.", + "explanation": "The source did not provide clearly parseable prefix data.", + "risk": "No reliable plausibility assessment can be made.", "recommendations": [ - "Prüfe das Prefix manuell in der zuständigen Registry.", + "Manually check the prefix in the relevant registry.", "Vergleiche die Daten mit einer alternativen Whois-/IRR-Quelle.", ], "raw": whois_payload, @@ -37,12 +37,12 @@ def check(self, prefix: str, origin_as: str | None, whois_payload: dict) -> dict if not route_origins: return { "status": CheckStatus.WARNING.value, - "summary": "Registry-Daten vorhanden, aber kein route/route6-Hinweis gefunden", - "explanation": "Es wurden allgemeine Whois-/Registry-Daten gefunden, jedoch kein klares route/route6-Objekt.", - "risk": "Ohne route/route6-Hinweis bleibt die Origin-Plausibilität eingeschränkt.", + "summary": "Registry data present, but no route/route6 indication found", + "explanation": "General Whois/Registry data was found, but no clear route/route6 object.", + "risk": "Without a route/route6 hint, origin plausibility remains limited.", "recommendations": [ - "Prüfe, ob ein passendes route/route6-Objekt in der IRR gepflegt ist.", - "Validiere die Origin-Zuordnung zusätzlich manuell.", + "Check whether a matching route/route6 object is maintained in the IRR.", + "Additionally validate the origin mapping manually.", ], "raw": whois_payload, } @@ -51,10 +51,10 @@ def check(self, prefix: str, origin_as: str | None, whois_payload: dict) -> dict return { "status": CheckStatus.OK.value, "summary": "Route/route6-Hinweise gefunden", - "explanation": "Es wurden route/route6-Objekte bzw. Origin-Hinweise zum Prefix gefunden. Ohne angegebenes Origin-AS erfolgt keine AS-Konsistenzprüfung.", - "risk": "Grundsätzliche Registry-Plausibilität ist gegeben, AS-Abgleich ist offen.", + "explanation": "route/route6 objects or origin hints were found for the prefix. Without a provided origin AS, no AS consistency check is performed.", + "risk": "General registry plausibility is present, but AS matching is still open.", "recommendations": [ - "Für eine strengere Prüfung optional ein Origin-AS mitgeben.", + "Optionally provide an origin AS for stricter validation.", ], "raw": whois_payload, } @@ -64,10 +64,10 @@ def check(self, prefix: str, origin_as: str | None, whois_payload: dict) -> dict return { "status": CheckStatus.OK.value, "summary": "Plausibles route/route6-Origin gefunden", - "explanation": f"Mindestens ein route/route6-Hinweis enthält das erwartete Origin {normalized_origin}.", + "explanation": f"At least one route/route6 hint contains the expected origin {normalized_origin}.", "risk": "Keine offensichtliche Registry-Inkonsistenz erkannt.", "recommendations": [ - "Registry-Daten regelmäßig aktuell halten.", + "Keep registry data regularly updated.", ], "raw": whois_payload, } @@ -76,10 +76,10 @@ def check(self, prefix: str, origin_as: str | None, whois_payload: dict) -> dict "status": CheckStatus.CRITICAL.value, "summary": "Route/route6-Origin widerspricht dem angegebenen Origin-AS", "explanation": f"Gefundene Origins: {', '.join(sorted(route_origins))}. Erwartet wurde {normalized_origin}.", - "risk": "Möglicher Konfigurations- oder Registry-Fehler mit Hijack-Risk.", + "risk": "Possible configuration or registry issue with hijack risk.", "recommendations": [ - "Origin-AS und route/route6-Objekte in der zuständigen Registry abgleichen.", - "Fehlerhafte Registry-Einträge korrigieren.", + "Cross-check origin AS and route/route6 objects in the relevant registry.", + "Correct incorrect registry entries.", ], "raw": whois_payload, } diff --git a/backend/app/services/roa_planner_service.py b/backend/app/services/roa_planner_service.py index a3b5741..bb76dc3 100644 --- a/backend/app/services/roa_planner_service.py +++ b/backend/app/services/roa_planner_service.py @@ -23,8 +23,8 @@ def check(self, prefix: str, origin_as: str, max_length: int | None = None) -> d if effective_max_length < prefix_length: return { "status": "CRITICAL", - "summary": "Max Length ist kleiner als die Prefixlänge und damit ungültig.", - "recommendations": ["Setzen Sie max_length mindestens auf die Prefixlänge.", "Prüfen Sie Prefix und Eingaben auf Tippfehler."], + "summary": "Max length is smaller than the prefix length and is therefore invalid.", + "recommendations": ["Set max_length to at least the prefix length.", "Check the prefix and inputs for typos."], "details": { "prefix": normalized_prefix, "origin_as": normalized_origin, @@ -74,23 +74,23 @@ def check(self, prefix: str, origin_as: str, max_length: int | None = None) -> d planned_validation_state = "valid" status = "OK" summary = "Geplantes Announcement ist durch mindestens eine passende ROA abgedeckt." - recommendations.append("Bestehende ROA-Abdeckung ist vorhanden; Änderungen außerhalb von RouteForge nur bei Bedarf durchführen.") + recommendations.append("Existing ROA coverage is present; apply changes outside RouteForge only if needed.") if conflicting_roas and not matching_roas: planned_validation_state = "invalid" status = "CRITICAL" summary = "Konfliktierende ROAs deuten auf ein invalides geplantes Announcement hin." - recommendations.append("Origin-AS oder Prefixplanung prüfen; bestehende ROA-Konflikte außerhalb von RouteForge bereinigen.") + recommendations.append("Review origin AS or prefix planning; resolve existing ROA conflicts outside RouteForge.") if effective_max_length > prefix_length + 2: max_length_risk = "broad" if status == "OK": status = "WARNING" - recommendations.append("Max Length ist relativ breit gewählt; reduzieren Sie die Breite, um Hijack-Risk zu senken.") + recommendations.append("Max length is relatively broad; reduce it to lower hijack risk.") suggested_roa = None if matching_roas else {"prefix": normalized_prefix, "origin_as": normalized_origin, "max_length": effective_max_length} if suggested_roa: - recommendations.append("ROA-Vorschlag extern durch LIR/Operator prüfen und außerhalb von RouteForge umsetzen.") + recommendations.append("Review the ROA proposal externally with LIR/operator and implement it outside RouteForge.") return { "status": status, @@ -116,8 +116,8 @@ def check(self, prefix: str, origin_as: str, max_length: int | None = None) -> d def _unknown(self, prefix: str, origin_as: str, max_length: int | None, effective_max_length: int, prefix_length: int, diagnostics: list[dict]) -> dict: return { "status": "UNKNOWN", - "summary": "RPKI-Datenquelle liefert aktuell keine belastbare Aussage.", - "recommendations": ["Später erneut prüfen.", "Externe ROA-Quelle manuell validieren."], + "summary": "RPKI data source currently does not provide a reliable result.", + "recommendations": ["Check again later.", "Manually validate against an external ROA source."], "details": { "prefix": prefix, "origin_as": origin_as, @@ -129,7 +129,7 @@ def _unknown(self, prefix: str, origin_as: str, max_length: int | None, effectiv "conflicting_roas": [], "suggested_roa": {"prefix": prefix, "origin_as": origin_as, "max_length": effective_max_length}, "max_length_risk": "unknown", - "recommendations": ["Später erneut prüfen.", "Externe ROA-Quelle manuell validieren."], + "recommendations": ["Check again later.", "Manually validate against an external ROA source."], "source_diagnostics": diagnostics, }, "input": {"prefix": prefix, "origin_as": origin_as, "max_length": max_length}, diff --git a/backend/app/services/routing_visibility_checker.py b/backend/app/services/routing_visibility_checker.py index 10bd9d2..65a9da7 100644 --- a/backend/app/services/routing_visibility_checker.py +++ b/backend/app/services/routing_visibility_checker.py @@ -26,10 +26,10 @@ def check(self, prefix: str, origin_as: str | None, routing_payload: dict | None "status": CheckStatus.UNKNOWN.value, "summary": "Routing visibility could not be determined", "explanation": "RIPEstat routing-status returned data, but RouteForge could not extract visible Origin-AS information from the response.", - "risk": "Die Datenstruktur war nicht eindeutig auswertbar.", + "risk": "The data structure could not be evaluated unambiguously.", "recommendations": [ - "Prüfe das Prefix zusätzlich über ein Looking Glass.", - "Prüfe, ob das Prefix aktuell announced werden soll.", + "Check the prefix with an additional looking glass.", + "Verify whether the prefix should currently be announced.", ], "raw": {**raw, "structure_unknown": True}, } @@ -38,9 +38,9 @@ def check(self, prefix: str, origin_as: str | None, routing_payload: dict | None return { "status": CheckStatus.OK.value, "summary": "Visible routing origins found", - "explanation": "Für das Prefix wurden sichtbare Origin-ASNs gefunden. Ohne erwartetes Origin-AS erfolgt kein Konsistenzabgleich.", - "risk": "Die Sichtbarkeit ist grundsätzlich erkennbar, aber die erwartete Origin-Zuordnung wurde nicht geprüft.", - "recommendations": ["Gib ein erwartetes Origin-AS an, um die Sichtbarkeit vollständig zu bewerten."], + "explanation": "Visible origin ASNs were found for this prefix. Without an expected origin AS, no consistency comparison is performed.", + "risk": "Visibility is generally confirmed, but the expected origin mapping was not validated.", + "recommendations": ["Provide an expected origin AS to fully evaluate visibility."], "raw": {**raw, "structure_unknown": True}, } @@ -50,19 +50,19 @@ def check(self, prefix: str, origin_as: str | None, routing_payload: dict | None "summary": "Prefix is visible with expected Origin-AS", "explanation": "Das Prefix wird mit dem erwarteten Origin-AS im Routing sichtbar.", "risk": "Keine offensichtliche Routing-Visibility-Inkonsistenz erkannt.", - "recommendations": ["Routing-Sichtbarkeit weiter überwachen."], + "recommendations": ["Continue monitoring routing visibility."], "raw": {**raw, "structure_unknown": True}, } return { "status": CheckStatus.CRITICAL.value, "summary": "Visible Origin-AS differs from expected Origin-AS", - "explanation": "Das Prefix ist sichtbar, aber nicht mit dem erwarteten Origin-AS.", - "risk": "Möglicher Routing-Fehler, falsches Announcement oder Hijack-Risk.", + "explanation": "The prefix is visible, but not with the expected origin AS.", + "risk": "Possible routing error, incorrect announcement, or hijack risk.", "recommendations": [ "Sichtbares Check the origin AS.", - "BGP Announcement und Upstream-Konfiguration prüfen.", - "RPKI und Registry/IRR-Daten gegenprüfen.", + "Check BGP announcement and upstream configuration.", + "Cross-check RPKI and Registry/IRR data.", ], "raw": raw, } @@ -74,9 +74,9 @@ def _result_unknown(self, payload: dict | None) -> dict: "explanation": "RIPEstat routing-status did not respond before the configured timeout or returned no usable payload.", "risk": "The assessment is incomplete.", "recommendations": [ - "Prüfe die Rohdaten.", + "Check the raw data.", "Retry the query later.", - "Vergleiche bei Bedarf mit einer zweiten Routing-Quelle oder einem Looking Glass.", + "Compare with a secondary routing source or a looking glass if needed.", ], "raw": {"routing_payload": payload or {}}, } diff --git a/backend/app/services/rpki_checker.py b/backend/app/services/rpki_checker.py index 55ed95a..88444c0 100644 --- a/backend/app/services/rpki_checker.py +++ b/backend/app/services/rpki_checker.py @@ -27,7 +27,7 @@ def check(self, prefix: str, origin_as: str | None) -> dict: "recommendations": [ "Review the API raw data.", "Repeat the check later.", - "Vergleiche bei Bedarf mit einer zweiten Quelle oder einem lokalen RPKI-Validator.", + "Compare with a secondary source or a local RPKI validator if needed.", ], } diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 146a8be..2e72624 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "routeforge-backend" -version = "0.9.2" +version = "1.0.0" 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 8f34688..f5bb036 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.2-rc' + assert payload.get('version') == 'v1.0.0' 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/release-checklist.md b/docs/operations/release-checklist.md index 4f721dd..305ea28 100644 --- a/docs/operations/release-checklist.md +++ b/docs/operations/release-checklist.md @@ -1,6 +1,6 @@ # Release Checklist -## v0.9.1-rc: Deployment QA & UX Validation +## v1.0.0: Deployment QA & UX Validation ### 1) Pre-release validation @@ -43,13 +43,13 @@ The script validates: ### 4) Tagging -- `git tag -a v0.9.1-rc -m "RouteForge v0.9.1-rc"` -- `git push origin v0.9.1-rc` +- `git tag -a v1.0.0 -m "RouteForge v1.0.0"` +- `git push origin v1.0.0` ### 5) GitHub Release -- Release title aligned with `v0.9.1-rc` -- Mark as prerelease +- Release title aligned with `v1.0.0` +- Mark as stable release - Include deployment, upgrade, and security QA notes ### 6) Post-release smoke test diff --git a/docs/operations/security.md b/docs/operations/security.md index 790b16f..aead857 100644 --- a/docs/operations/security.md +++ b/docs/operations/security.md @@ -1,6 +1,6 @@ # Security Baseline -## Security QA checklist (v0.9.1-rc) +## Security QA checklist (v1.0.0) - [ ] `SECRET_KEY` changed from any default/dev value - [ ] `COOKIE_SECURE=true` when running behind HTTPS diff --git a/docs/operations/troubleshooting.md b/docs/operations/troubleshooting.md index 47370e6..4a16361 100644 --- a/docs/operations/troubleshooting.md +++ b/docs/operations/troubleshooting.md @@ -2,16 +2,16 @@ ## sqlite3.OperationalError: attempt to write a readonly database -### Ursache +### Cause -Bei SQLite-Deployments liegt die Datenbank häufig unter `/app/data/routeforge.db`. -Wenn das Docker-Volume auf `/app/data` root-owned ist, kann der non-root Runtime-User `routeforge` nicht in die SQLite-Datei schreiben. Dadurch schlagen Check-Speicherungen mit `sqlite3.OperationalError: attempt to write a readonly database` fehl. +In SQLite deployments, the database is commonly stored at `/app/data/routeforge.db`. +If the Docker volume at `/app/data` is owned by root, the non-root runtime user `routeforge` cannot write to the SQLite file. Check persistence then fails with `sqlite3.OperationalError: attempt to write a readonly database`. -### Fix ab v0.5.5-beta +### Fix since v0.5.5-beta -Ab `v0.5.5-beta` setzt der Backend-Entrypoint beim Containerstart die Ownership für `/app/data` auf `routeforge:routeforge` und startet danach den Prozess weiterhin als non-root User `routeforge`. +Since `v0.5.5-beta`, the backend entrypoint sets ownership for `/app/data` to `routeforge:routeforge` at container startup and then keeps running as non-root user `routeforge`. -### Workaround für ältere Versionen +### Workaround for older versions ```bash docker compose down @@ -22,39 +22,39 @@ docker compose up -d --build ## KeyError: 'formatters' (Alembic) -### Ursache +### Cause -Ältere Versionen hatten eine minimale `backend/alembic.ini` ohne Logging-Sektionen, während `backend/alembic/env.py` `fileConfig(...)` aufgerufen hat. +Older versions used a minimal `backend/alembic.ini` without logging sections while `backend/alembic/env.py` still called `fileConfig(...)`. ### Fix -Update auf `v0.6.4-beta` oder neuer, dann: +Upgrade to `v0.6.4-beta` or newer, then: ```bash docker compose exec backend alembic upgrade head ``` -## Ich sehe keine Check-Menüpunkte +## I do not see check menu items -Rolle prüfen: `viewer` sieht nur Dashboard/Reports/About. +Check role permissions: `viewer` can only see Dashboard/Reports/About. -## 403 bei Checks +## 403 for checks -User ist `viewer` oder inaktiv. Rolle und `is_active` im Admin User Management prüfen. +The user is `viewer` or inactive. Verify role and `is_active` in Admin User Management. -## Login geht nicht +## Login does not work -Prüfen: User aktiv? Passwort korrekt? Wurde `SECRET_KEY` geändert? +Check: Is the user active? Is the password correct? Was `SECRET_KEY` changed? -## Nach SECRET_KEY Änderung +## After SECRET_KEY change -Alle Sessions sind ungültig. Bitte neu einloggen. +All sessions become invalid. Log in again. ## Migration required in UI/System Status -Wenn `migration_status=behind` gemeldet wird, führe nacheinander aus: +If `migration_status=behind` is reported, run in order: ```bash alembic current @@ -62,4 +62,4 @@ alembic heads alembic upgrade head ``` -Nur wenn Schema bereits existiert und exakt zum Baseline-Stand passt: `alembic stamp 0001_initial_schema`. +Use `alembic stamp 0001_initial_schema` only when the schema already exists and exactly matches the baseline state. diff --git a/docs/operations/upgrades.md b/docs/operations/upgrades.md index aea7efc..b248b51 100644 --- a/docs/operations/upgrades.md +++ b/docs/operations/upgrades.md @@ -9,7 +9,7 @@ git pull docker compose -f docker-compose.prod.yml build ``` -## Upgrade QA matrix (v0.9.1-rc) +## Upgrade QA matrix (v1.0.0) ### A) Fresh SQLite DB diff --git a/frontend/package.json b/frontend/package.json index 6d1d785..1c74cb2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "routeforge-frontend", - "version": "0.9.2", + "version": "1.0.0", "private": true, "license": "AGPL-3.0-or-later", "type": "module", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c746bf5..6af2612 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -74,7 +74,7 @@ export default function App() { if (authMode === 'login') return if (authMode === 'error') return
{authError}
- const systemLine = system ? `${system.name} ${system.version} · mode=${system.demo_mode ? 'DEMO' : 'LIVE'} · read_only=${String(system.read_only)}` : 'RouteForge v0.9.2-rc · read-only preflight checks' + const systemLine = system ? `${system.name} ${system.version} · mode=${system.demo_mode ? 'DEMO' : 'LIVE'} · read_only=${String(system.read_only)}` : 'RouteForge v1.0.0 · read-only preflight checks' 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' @@ -88,8 +88,8 @@ export default function App() { } const readOnlyLabel = system?.read_only ? 'Enabled' : 'Disabled' - return - {active === 'dashboard' &&

{system?.name || 'RouteForge'} {system?.version || 'v0.9.2-rc'}

Current user: {currentUser?.username || '-'}

Role: {currentUser?.role || '-'}

Read-only: {readOnlyLabel}

Mode: {system?.demo_mode ? 'DEMO' : 'LIVE'}

{system?.demo_mode &&

Demo mode is active.

}

Quick Actions

{role === 'admin' && }
{migrationsBlocked &&
Database migrations are required before using RouteForge. Run: alembic current, alembic heads, alembic upgrade head.
}
} + return + {active === 'dashboard' &&

{system?.name || 'RouteForge'} {system?.version || 'v1.0.0'}

Current user: {currentUser?.username || '-'}

Role: {currentUser?.role || '-'}

Read-only: {readOnlyLabel}

Mode: {system?.demo_mode ? 'DEMO' : 'LIVE'}

{system?.demo_mode &&

Demo mode is active.

}

Quick Actions

{role === 'admin' && }
{migrationsBlocked &&
Database migrations are required before using RouteForge. Run: alembic current, alembic heads, alembic upgrade head.
}
} {!canAccess(active) &&
You do not have permission to access this section.
} {active === 'asn' && canAccess('asn') && } {active === 'prefix' && canAccess('prefix') && } @@ -102,6 +102,6 @@ export default function App() { {active === 'system' && canAccess('system') &&
{systemStatusError &&
{systemStatusError}
}{migrationsBlocked &&
Database migrations are required before using RouteForge. Run: alembic current, alembic heads, alembic upgrade head.
}{systemStatus &&
Version: {systemStatus.version}
Mode: {systemStatus.mode}
API Proxy: {proxyStatus}
Migration Status: {migrationStatus}
DB Current Revision: {systemStatus.database?.schema_version || 'unknown'}
DB Head Revision: {systemStatus.database?.migration_head || 'unknown'}
}
} {active === 'users' && canAccess('users') && } {active === 'audit' && canAccess('audit') && } - {active === 'about' &&

RouteForge is a read-only routing operations console for validation workflows.

Read-only: All checks are non-destructive.

Version: v0.9.2-rc

} + {active === 'about' &&

RouteForge is a read-only routing operations console for validation workflows.

Read-only: All checks are non-destructive.

Version: {system?.version || 'v1.0.0'}

}
} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 664b021..fb83c0b 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -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 }: { children: ReactNode; active: NavKey; onNav: (key: NavKey) => void; systemLine: string; title: string; demoMode: boolean; currentUser?: { username: string; role: UserRole } | null; onLogout: () => void }) { +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 }) { const visibleNav = nav.filter((item) => { if (!currentUser) return ['dashboard', 'about'].includes(item.key) if (currentUser.role === 'admin') return true @@ -48,7 +48,7 @@ export function Layout({ children, active, onNav, systemLine, title, demoMode, c {currentUser && Signed in as {currentUser.username} · {currentUser.role}} {demoMode ? 'DEMO' : 'LIVE'} READ-ONLY - v0.9.2-rc + {appVersion || 'v1.0.0'}