diff --git a/.env.example b/.env.example index b52d5b0..9a2eee0 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -# General +# Core / App ROUTEFORGE_DEMO_MODE=false LOG_LEVEL=INFO @@ -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 @@ -25,15 +16,6 @@ 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 @@ -41,17 +23,17 @@ 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= @@ -59,6 +41,17 @@ 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 diff --git a/backend/app/templates/report.html.j2 b/backend/app/templates/report.html.j2 index 8e8b5ba..5464c17 100644 --- a/backend/app/templates/report.html.j2 +++ b/backend/app/templates/report.html.j2 @@ -1,11 +1,11 @@

RouteForge Report

-

Zeitpunkt: {{ report.generated_at }}

+

Generated At: {{ report.generated_at }}

Check-ID: {{ report.check_id }}

-

Eingabe

+

Input

-

Gesamtbewertung

+

Overall Assessment

Data Source Diagnostics

-{% if report.details and report.details.source_diagnostics %}{% for d in report.details.source_diagnostics %}{% endfor %}
QuelleEndpointStatusFreshnessAttemptsRetriesFallbackStale CacheDauerCacheCache AgeTTLMessage
{{ 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 %}{% if d.details %}
Details
{{ d.details | tojson(indent=2) }}
{% endif %}
{% else %}

No source diagnostics available.

{% endif %} +{% if report.details and report.details.source_diagnostics %}{% for d in report.details.source_diagnostics %}{% endfor %}
SourceEndpointStatusFreshnessAttemptsRetriesFallbackStale CacheDurationCacheCache AgeTTLMessage
{{ 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 %}{% if d.details %}
Details
{{ d.details | tojson(indent=2) }}
{% endif %}
{% else %}

No source diagnostics available.

{% endif %} -

Rohdaten

-

RPKI Rohdaten

+

Raw Data

+

RPKI Raw Data

{{ (report.checks.rpki.raw if report.checks and report.checks.rpki else {}) | tojson(indent=2) }}
-

Registry/IRR Rohdaten

+

Registry/IRR Raw Data

{{ (report.checks.registry.raw if report.checks and report.checks.registry else {}) | tojson(indent=2) }}
-

Routing Visibility Rohdaten

-
Ein-/ausklappen
{{ (report.checks.routing_visibility.raw if report.checks and report.checks.routing_visibility else {}) | tojson(indent=2) }}
+

Routing Visibility Raw Data

+
Expand/collapse
{{ (report.checks.routing_visibility.raw if report.checks and report.checks.routing_visibility else {}) | tojson(indent=2) }}
-

Technische Details

{{ report.details | tojson(indent=2) }}
+

Technical Details

{{ report.details | tojson(indent=2) }}
{% if report.details and report.details.rpki_summary %}

RPKI Batch Summary

{{ report.details.rpki_summary | tojson(indent=2) }}
{% endif %} -{% if report.details and report.details.results %}

RPKI Batch Ergebnisse

{{ report.details.results | tojson(indent=2) }}
{% endif %} +{% if report.details and report.details.results %}

RPKI Batch Results

{{ report.details.results | tojson(indent=2) }}
{% endif %} {% if report.details and report.details.preflight_mode %}

Preflight Assessment

{% endif %} diff --git a/backend/app/templates/report.md.j2 b/backend/app/templates/report.md.j2 index 3137c70..dd596aa 100644 --- a/backend/app/templates/report.md.j2 +++ b/backend/app/templates/report.md.j2 @@ -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 '-' }} @@ -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) }} ``` @@ -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) }} ``` diff --git a/backend/tests/test_release_cleanup_guards.py b/backend/tests/test_release_cleanup_guards.py new file mode 100644 index 0000000..aa7ed3e --- /dev/null +++ b/backend/tests/test_release_cleanup_guards.py @@ -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 "

Input

" in template + assert "

Overall Assessment

" in template + assert "

Raw Data

" in template + assert "

Technical Details

" in template