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
43 changes: 18 additions & 25 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# General
# Core / App
ROUTEFORGE_DEMO_MODE=false
LOG_LEVEL=INFO

Expand All @@ -8,15 +8,6 @@ POSTGRES_USER=routeforge
POSTGRES_PASSWORD=change-me
DATABASE_URL=postgresql+psycopg://routeforge:change-me@postgres:5432/routeforge

# CORS (comma-separated list; mainly needed for split frontend/backend deployments)
CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000

# Optional. Leave empty for same-origin /api proxy via frontend nginx.
VITE_API_URL=

# Frontend dev proxy target (for `frontend/vite` local development)
VITE_API_PROXY_TARGET=http://backend:8000

# RIPEstat
RIPESTAT_BASE_URL=https://stat.ripe.net/data
RIPESTAT_CACHE_TTL_SECONDS=900
Expand All @@ -25,40 +16,42 @@ RIPESTAT_MAX_RETRIES=1
RIPESTAT_RETRY_BACKOFF_SECONDS=0.5
RIPESTAT_USE_STALE_CACHE_ON_ERROR=true

# Optional general HTTP timeout for backend calls
HTTP_TIMEOUT_SECONDS=10

# Auth
SECRET_KEY=change-me
SESSION_COOKIE_NAME=routeforge_session
SESSION_EXPIRE_HOURS=12
COOKIE_SECURE=false

# RPKI Provider
RPKI_PROVIDER=ripestat
RPKI_ROUTINATOR_URL=http://routinator:8323
RPKI_LOCAL_JSON_PATH=
RPKI_PROVIDER_TIMEOUT_SECONDS=5
RPKI_FALLBACK_TO_RIPESTAT=true

# BGP Multi-source
# BGP Visibility Provider
BGP_VISIBILITY_PROVIDERS=ripestat
BGP_GENERIC_URL_TEMPLATE=
BGP_PROVIDER_TIMEOUT_SECONDS=5
BGP_VISIBILITY_REQUIRE_SOURCE_AGREEMENT=false
BGP_VISIBILITY_MIN_CONFIDENCE=60

# Change workflow
# Change / Post-Change
POST_CHANGE_DEFAULT_RECHECK_MINUTES=15,30,60

# Watch alerts
# Alerts / Webhooks
ALERT_WEBHOOK_ENABLED=false
ALERT_WEBHOOK_URL=
ALERT_WEBHOOK_SECRET=
ALERT_ON_STATUS_CHANGE_ONLY=true
ALERT_WEBHOOK_TIMEOUT_SECONDS=5
ALERT_WEBHOOK_MAX_RETRIES=1

# Watch alerting
ALERT_WEBHOOK_MAX_RETRIES=2
ALERT_WEBHOOK_TIMEOUT_SECONDS=5
# Security / CORS
# CORS origins (comma-separated list; mainly needed for split frontend/backend deployments)
CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
# Optional. Leave empty for same-origin /api proxy via frontend nginx.
VITE_API_URL=
# Frontend dev proxy target (for `frontend/vite` local development)
VITE_API_PROXY_TARGET=http://backend:8000
# Optional general HTTP timeout for backend calls
HTTP_TIMEOUT_SECONDS=10
# Auth
SECRET_KEY=change-me
SESSION_COOKIE_NAME=routeforge_session
SESSION_EXPIRE_HOURS=12
COOKIE_SECURE=false
26 changes: 13 additions & 13 deletions backend/app/templates/report.html.j2
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
<!doctype html><html><body>
<h1>RouteForge Report</h1>
<p><strong>Zeitpunkt:</strong> {{ report.generated_at }}</p>
<p><strong>Generated At:</strong> {{ report.generated_at }}</p>
<p><strong>Check-ID:</strong> {{ report.check_id }}</p>

<h2>Eingabe</h2>
<h2>Input</h2>
<ul>
<li><strong>Check-Typ:</strong> {{ report.input_check_type or '-' }}</li>
<li><strong>Check Type:</strong> {{ report.input_check_type or '-' }}</li>
<li><strong>Prefix:</strong> {{ report.input.prefix if report.input else '-' }}</li>
<li><strong>Origin-AS:</strong> {{ report.input.origin_as if report.input and report.input.origin_as else '-' }}</li>
<li><strong>Planned Origin-AS:</strong> {{ report.input.planned_origin_as if report.input and report.input.planned_origin_as else '-' }}</li>
<li><strong>ASN:</strong> {{ report.input.asn if report.input and report.input.asn else '-' }}</li>
<li><strong>Holder:</strong> {{ report.details.resource_holder.holder if report.details and report.details.resource_holder else 'Unknown' }}</li>
</ul>

<h2>Gesamtbewertung</h2>
<h2>Overall Assessment</h2>
<ul>
<li><strong>Status:</strong> {{ report.status }}</li>
<li><strong>Kurzfassung:</strong> {{ report.summary }}</li>
<li><strong>Summary:</strong> {{ report.summary }}</li>
<li><strong>Preflight Decision:</strong> {{ report.details.preflight_decision if report.details and report.details.preflight_decision else '-' }}</li>
<li><strong>Explanation:</strong> {{ report.explanation or '-' }}</li>
<li><strong>Risk:</strong> {{ report.risk or '-' }}</li>
Expand Down Expand Up @@ -65,20 +65,20 @@
</ul>

<h2>Data Source Diagnostics</h2>
{% if report.details and report.details.source_diagnostics %}<table border="1" cellpadding="6" cellspacing="0"><tr><th>Quelle</th><th>Endpoint</th><th>Status</th><th>Freshness</th><th>Attempts</th><th>Retries</th><th>Fallback</th><th>Stale Cache</th><th>Dauer</th><th>Cache</th><th>Cache Age</th><th>TTL</th><th>Message</th></tr>{% for d in report.details.source_diagnostics %}<tr><td>{{ d.name or '-' }}</td><td>{{ d.endpoint or '-' }}</td><td>{{ d.status or '-' }}</td><td>{{ d.freshness or 'UNKNOWN' }}</td><td>{{ d.attempts if d.attempts is not none else '-' }}</td><td>{{ d.retry_count if d.retry_count is not none else '-' }}</td><td>{{ 'yes' if d.fallback_used is sameas true else ('no' if d.fallback_used is sameas false else '-') }}</td><td>{{ 'yes' if d.stale_cache_used is sameas true else ('no' if d.stale_cache_used is sameas false else '-') }}</td><td>{{ d.duration_ms if d.duration_ms is not none else '-' }}</td><td>{{ 'HIT' if d.cached is sameas true else ('MISS' if d.cached is sameas false else '-') }}</td><td>{{ d.cache_age_seconds if d.cache_age_seconds is not none else 'Unknown' }}</td><td>{{ d.cache_ttl_seconds if d.cache_ttl_seconds is not none else 'Unknown' }}</td><td>{{ d.message or '-' }}{% if d.fallback_used is sameas true %}<div><em>Live request failed. Cached data was used.</em></div>{% endif %}{% if d.details %}<details><summary>Details</summary><pre>{{ d.details | tojson(indent=2) }}</pre></details>{% endif %}</td></tr>{% endfor %}</table>{% else %}<p>No source diagnostics available.</p>{% endif %}
{% if report.details and report.details.source_diagnostics %}<table border="1" cellpadding="6" cellspacing="0"><tr><th>Source</th><th>Endpoint</th><th>Status</th><th>Freshness</th><th>Attempts</th><th>Retries</th><th>Fallback</th><th>Stale Cache</th><th>Duration</th><th>Cache</th><th>Cache Age</th><th>TTL</th><th>Message</th></tr>{% for d in report.details.source_diagnostics %}<tr><td>{{ d.name or '-' }}</td><td>{{ d.endpoint or '-' }}</td><td>{{ d.status or '-' }}</td><td>{{ d.freshness or 'UNKNOWN' }}</td><td>{{ d.attempts if d.attempts is not none else '-' }}</td><td>{{ d.retry_count if d.retry_count is not none else '-' }}</td><td>{{ 'yes' if d.fallback_used is sameas true else ('no' if d.fallback_used is sameas false else '-') }}</td><td>{{ 'yes' if d.stale_cache_used is sameas true else ('no' if d.stale_cache_used is sameas false else '-') }}</td><td>{{ d.duration_ms if d.duration_ms is not none else '-' }}</td><td>{{ 'HIT' if d.cached is sameas true else ('MISS' if d.cached is sameas false else '-') }}</td><td>{{ d.cache_age_seconds if d.cache_age_seconds is not none else 'Unknown' }}</td><td>{{ d.cache_ttl_seconds if d.cache_ttl_seconds is not none else 'Unknown' }}</td><td>{{ d.message or '-' }}{% if d.fallback_used is sameas true %}<div><em>Live request failed. Cached data was used.</em></div>{% endif %}{% if d.details %}<details><summary>Details</summary><pre>{{ d.details | tojson(indent=2) }}</pre></details>{% endif %}</td></tr>{% endfor %}</table>{% else %}<p>No source diagnostics available.</p>{% endif %}

<h2>Rohdaten</h2>
<h3>RPKI Rohdaten</h3>
<h2>Raw Data</h2>
<h3>RPKI Raw Data</h3>
<pre>{{ (report.checks.rpki.raw if report.checks and report.checks.rpki else {}) | tojson(indent=2) }}</pre>
<h3>Registry/IRR Rohdaten</h3>
<h3>Registry/IRR Raw Data</h3>
<pre>{{ (report.checks.registry.raw if report.checks and report.checks.registry else {}) | tojson(indent=2) }}</pre>
<h3>Routing Visibility Rohdaten</h3>
<details><summary>Ein-/ausklappen</summary><pre>{{ (report.checks.routing_visibility.raw if report.checks and report.checks.routing_visibility else {}) | tojson(indent=2) }}</pre></details>
<h3>Routing Visibility Raw Data</h3>
<details><summary>Expand/collapse</summary><pre>{{ (report.checks.routing_visibility.raw if report.checks and report.checks.routing_visibility else {}) | tojson(indent=2) }}</pre></details>

<h2>Technische Details</h2><pre>{{ report.details | tojson(indent=2) }}</pre>
<h2>Technical Details</h2><pre>{{ report.details | tojson(indent=2) }}</pre>

{% if report.details and report.details.rpki_summary %}<h2>RPKI Batch Summary</h2><pre>{{ report.details.rpki_summary | tojson(indent=2) }}</pre>{% endif %}
{% if report.details and report.details.results %}<h2>RPKI Batch Ergebnisse</h2><pre>{{ report.details.results | tojson(indent=2) }}</pre>{% endif %}
{% if report.details and report.details.results %}<h2>RPKI Batch Results</h2><pre>{{ report.details.results | tojson(indent=2) }}</pre>{% endif %}
</body></html>

{% if report.details and report.details.preflight_mode %}<h2>Preflight Assessment</h2><ul><li><strong>Planned Prefix:</strong> {{ report.input.prefix if report.input else '-' }}</li><li><strong>Planned Origin-AS:</strong> {{ report.input.planned_origin_as if report.input and report.input.planned_origin_as else '-' }}</li><li><strong>Overall Status:</strong> {{ report.status }}</li><li><strong>Summary:</strong> {{ report.summary }}</li><li><strong>Explanation:</strong> {{ report.explanation or '-' }}</li><li><strong>Risk:</strong> {{ report.risk or '-' }}</li></ul>{% endif %}
24 changes: 12 additions & 12 deletions backend/app/templates/report.md.j2
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
# RouteForge Report

- Zeitpunkt: {{ report.generated_at }}
- Generated At: {{ report.generated_at }}
- Check-ID: {{ report.check_id }}

## Eingabe
- Check-Typ: {{ report.input_check_type or '-' }}
## Input
- Check Type: {{ report.input_check_type or '-' }}
- Prefix: {{ report.input.prefix if report.input else '-' }}
- Origin-AS: {{ report.input.origin_as if report.input and report.input.origin_as else '-' }}
- Planned Origin-AS: {{ report.input.planned_origin_as if report.input and report.input.planned_origin_as else '-' }}
- ASN: {{ report.input.asn if report.input and report.input.asn else '-' }}
- Holder: {{ report.details.resource_holder.holder if report.details and report.details.resource_holder else 'Unknown' }}

## Gesamtbewertung
## Overall Assessment
- Status: **{{ report.status }}**
- Kurzfassung: {{ report.summary }}
- Summary: {{ report.summary }}
- Preflight Decision: {{ report.details.preflight_decision if report.details and report.details.preflight_decision else '-' }}
- Explanation: {{ report.explanation or '-' }}
- Risk: {{ report.risk or '-' }}
Expand Down Expand Up @@ -57,30 +57,30 @@

## Data Source Diagnostics
{% if report.details and report.details.source_diagnostics %}
| Quelle | Endpoint | Status | Freshness | Attempts | Retries | Fallback | Stale Cache | Dauer | Cache | Cache Age | TTL | Message |
| Source | Endpoint | Status | Freshness | Attempts | Retries | Fallback | Stale Cache | Duration | Cache | Cache Age | TTL | Message |
|---|---|---|---:|---|---|
{% for d in report.details.source_diagnostics %}| {{ d.name or '-' }} | {{ d.endpoint or '-' }} | {{ d.status or '-' }} | {{ d.freshness or 'UNKNOWN' }} | {{ d.attempts if d.attempts is not none else '-' }} | {{ d.retry_count if d.retry_count is not none else '-' }} | {{ 'yes' if d.fallback_used is sameas true else ('no' if d.fallback_used is sameas false else '-') }} | {{ 'yes' if d.stale_cache_used is sameas true else ('no' if d.stale_cache_used is sameas false else '-') }} | {{ d.duration_ms if d.duration_ms is not none else '-' }} | {{ 'HIT' if d.cached is sameas true else ('MISS' if d.cached is sameas false else '-') }} | {{ d.cache_age_seconds if d.cache_age_seconds is not none else 'Unknown' }} | {{ d.cache_ttl_seconds if d.cache_ttl_seconds is not none else 'Unknown' }} | {{ d.message or '-' }}{% if d.fallback_used is sameas true %} (Live request failed. Cached data was used.){% endif %} |
{% endfor %}
{% else %}No source diagnostics available.
{% endif %}

## Rohdaten
### RPKI Rohdaten
## Raw Data
### RPKI Raw Data
```json
{{ (report.checks.rpki.raw if report.checks and report.checks.rpki else {}) | tojson(indent=2) }}
```

### Registry/IRR Rohdaten
### Registry/IRR Raw Data
```json
{{ (report.checks.registry.raw if report.checks and report.checks.registry else {}) | tojson(indent=2) }}
```

### Routing Visibility Rohdaten
### Routing Visibility Raw Data
```json
{{ (report.checks.routing_visibility.raw if report.checks and report.checks.routing_visibility else {}) | tojson(indent=2) }}
```

## Technische Details
## Technical Details
```json
{{ report.details | tojson(indent=2) }}
```
Expand All @@ -93,7 +93,7 @@
{% endif %}

{% if report.details and report.details.results %}
## RPKI Batch Ergebnisse
## RPKI Batch Results
```json
{{ report.details.results | tojson(indent=2) }}
```
Expand Down
35 changes: 35 additions & 0 deletions backend/tests/test_release_cleanup_guards.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from pathlib import Path


def _parse_env_keys(path: Path) -> list[str]:
keys: list[str] = []
for raw_line in path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _ = line.split("=", 1)
keys.append(key.strip())
return keys


def test_env_example_has_no_duplicate_keys() -> None:
env_path = Path(__file__).resolve().parents[2] / ".env.example"
keys = _parse_env_keys(env_path)
duplicates = sorted({key for key in keys if keys.count(key) > 1})
assert duplicates == []


def test_markdown_report_template_uses_english_headings() -> None:
template = (Path(__file__).resolve().parents[1] / "app" / "templates" / "report.md.j2").read_text(encoding="utf-8")
assert "## Input" in template
assert "## Overall Assessment" in template
assert "## Raw Data" in template
assert "## Technical Details" in template


def test_html_report_template_uses_english_headings() -> None:
template = (Path(__file__).resolve().parents[1] / "app" / "templates" / "report.html.j2").read_text(encoding="utf-8")
assert "<h2>Input</h2>" in template
assert "<h2>Overall Assessment</h2>" in template
assert "<h2>Raw Data</h2>" in template
assert "<h2>Technical Details</h2>" in template
Loading