diff --git a/.env.example b/.env.example index 04e0ad5..b52d5b0 100644 --- a/.env.example +++ b/.env.example @@ -58,3 +58,7 @@ 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 diff --git a/README.md b/README.md index 8c99426..34971c4 100644 --- a/README.md +++ b/README.md @@ -304,3 +304,11 @@ In the standard Docker setup, API calls are same-origin via frontend nginx (`/ap ## BGP Visibility Details - Read-only BGP visibility validation for prefix and optional expected origin AS. - Uses external RIPEstat visibility data; results are momentary snapshots and do not replace continuous monitoring. + + +## Operational docs for provider/workflow extensions +- docs/operations/rpki-provider.md +- docs/operations/bgp-visibility-providers.md +- docs/operations/change-case-workflow.md +- docs/operations/watch-alerts.md +- docs/architecture/providers.md diff --git a/backend/app/api/routes_change_cases.py b/backend/app/api/routes_change_cases.py index 37584a8..b5e7904 100644 --- a/backend/app/api/routes_change_cases.py +++ b/backend/app/api/routes_change_cases.py @@ -60,8 +60,8 @@ def patch_change_case(change_case_id: int, payload: ChangeCaseUpdate, db: Sessio def run_change_case_preflight(change_case_id: int, db: Session = Depends(get_db), user=Depends(require_role('operator','admin'))): cc = db.query(ChangeCase).filter(ChangeCase.id == change_case_id).first() if not cc: raise HTTPException(status_code=404, detail='Change Case not found') - prefixes = cc.affected_prefixes or [] - origins = cc.planned_origin_asns or [] + prefixes = [str(p).strip() for p in (cc.affected_prefixes or []) if str(p).strip()] + origins = [str(o).strip() for o in (cc.planned_origin_asns or []) if str(o).strip()] if not prefixes or not origins: raise HTTPException(status_code=400, detail='Change case requires affected_prefixes and planned_origin_asns') decisions=[]; actions=[] for pfx in prefixes: @@ -72,6 +72,8 @@ def run_change_case_preflight(change_case_id: int, db: Session = Depends(get_db) actions.append(f'Review preflight findings for {pfx} {origin}') cc.last_preflight_at=datetime.utcnow(); cc.required_actions=sorted(set(actions)) cc.decision='NO-GO' if 'CRITICAL' in decisions else 'CAUTION' if 'WARNING' in decisions else 'UNKNOWN' if all(d=='UNKNOWN' for d in decisions) else 'GO' + if cc.decision not in ALLOWED_DECISIONS: + cc.decision = "UNKNOWN" cc.risk_summary=f'Automated preflight decision: {cc.decision}' db.commit(); db.refresh(cc) write_audit_log(db, user_id=user.id, action='change_case_preflight_completed', target_type='change_case', target_id=str(cc.id), details_json={'decision': cc.decision}) @@ -82,6 +84,8 @@ def run_post_change_verification(change_case_id: int, db: Session = Depends(get_ cc = db.query(ChangeCase).filter(ChangeCase.id == change_case_id).first() if not cc: raise HTTPException(status_code=404, detail='Change Case not found') status='VERIFIED' if cc.decision=='GO' else 'PARTIAL' if cc.decision=='CAUTION' else 'FAILED' if cc.decision=='NO-GO' else 'UNKNOWN' + if status not in ALLOWED_POST_CHANGE_STATUSES: + status = "UNKNOWN" cc.post_change_status=status; cc.last_verification_at=datetime.utcnow() db.commit(); db.refresh(cc) write_audit_log(db, user_id=user.id, action='post_change_verification_completed', target_type='change_case', target_id=str(cc.id), details_json={'post_change_status': status}) @@ -112,3 +116,5 @@ def list_change_case_reports(change_case_id: int, db: Session = Depends(get_db), .all() ) return [{'report_id': r.id, 'check_id': c.id, 'check_type': c.check_type, 'summary': c.summary, 'status': c.status, 'created_at': r.created_at.isoformat()} for r, c in rows] +ALLOWED_DECISIONS = {"GO", "CAUTION", "NO-GO", "UNKNOWN"} +ALLOWED_POST_CHANGE_STATUSES = {"VERIFIED", "PARTIAL", "FAILED", "UNKNOWN"} diff --git a/backend/app/core/system_status.py b/backend/app/core/system_status.py index 92cb196..30d5a19 100644 --- a/backend/app/core/system_status.py +++ b/backend/app/core/system_status.py @@ -161,7 +161,13 @@ def build_system_status(engine: Engine | None) -> dict: "api_proxy": {"status": "ok", "mode": "same-origin", "frontend_proxy_expected": True}, "rpki": {"provider": settings.rpki_provider, "fallback_to_ripestat": settings.rpki_fallback_to_ripestat, "routinator_url": settings.rpki_routinator_url, "local_json_path": settings.rpki_local_json_path, "timeout_seconds": settings.rpki_provider_timeout_seconds}, "bgp_visibility": {"providers": [x.strip() for x in settings.bgp_visibility_providers.split(",") if x.strip()], "require_source_agreement": settings.bgp_visibility_require_source_agreement, "min_confidence": settings.bgp_visibility_min_confidence}, - "alerts": {"webhook_enabled": settings.alert_webhook_enabled, "webhook_url_configured": bool(settings.alert_webhook_url), "on_status_change_only": settings.alert_on_status_change_only}, + "alerts": { + "webhook_enabled": settings.alert_webhook_enabled, + "webhook_url_configured": bool(settings.alert_webhook_url), + "on_status_change_only": settings.alert_on_status_change_only, + "max_retries": settings.alert_webhook_max_retries, + "timeout_seconds": settings.alert_webhook_timeout_seconds, + }, "ripestat": { "cache_ttl_seconds": settings.cache_ttl_seconds, "timeout_seconds": settings.ripestat_timeout_seconds, diff --git a/backend/app/schemas.py b/backend/app/schemas.py index e99d592..55367c5 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -25,6 +25,17 @@ class ChangeCaseRead(BaseModel): created_by_user_id: int | None created_at: datetime updated_at: datetime + affected_prefixes: list[str] | None = None + planned_origin_asns: list[str] | None = None + risk_summary: str | None = None + decision: str | None = None + required_actions: list[str] | None = None + post_change_status: str | None = None + last_preflight_at: datetime | None = None + last_verification_at: datetime | None = None + + class Config: + from_attributes = True class AsnCheckRequest(BaseModel): @@ -207,6 +218,9 @@ class WatchRunRead(BaseModel): status: str changed: bool summary: str + alert_delivery_status: str | None = None + alert_delivered_at: datetime | None = None + alert_error_message: str | None = None created_at: datetime class Config: from_attributes = True diff --git a/backend/app/templates/report.html.j2 b/backend/app/templates/report.html.j2 index c7d32ad..8e8b5ba 100644 --- a/backend/app/templates/report.html.j2 +++ b/backend/app/templates/report.html.j2 @@ -48,6 +48,21 @@
  • Explanation: {{ report.checks.routing_visibility.explanation if report.checks and report.checks.routing_visibility else '-' }}
  • Risk: {{ report.checks.routing_visibility.risk if report.checks and report.checks.routing_visibility else '-' }}
  • +

    Extended Routing/Validation Details

    +

    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 %} diff --git a/backend/app/templates/report.md.j2 b/backend/app/templates/report.md.j2 index 4ff4cee..3137c70 100644 --- a/backend/app/templates/report.md.j2 +++ b/backend/app/templates/report.md.j2 @@ -41,6 +41,20 @@ - Explanation: {{ report.checks.routing_visibility.explanation if report.checks and report.checks.routing_visibility else '-' }} - Risk: {{ report.checks.routing_visibility.risk if report.checks and report.checks.routing_visibility else '-' }} +## Extended Routing/Validation Details +- RPKI Provider: {{ report.details.provider if report.details and report.details.provider else '-' }} +- RPKI Validation Status: {{ report.details.raw_status if report.details and report.details.raw_status is not none else '-' }} +- RPKI Fallback Used: {{ report.details.fallback_used if report.details else '-' }} +- RPKI Fallback Reason: {{ report.details.fallback_reason if report.details and report.details.fallback_reason else '-' }} +- RPKI Provider Disagreement: {{ report.details.provider_disagreement if report.details else '-' }} +- Matched ROAs: {{ (report.details.matched_roas | length) if report.details and report.details.matched_roas else 0 }} +- BGP Source Agreement: {{ report.details.source_agreement if report.details and report.details.source_agreement is not none else '-' }} +- BGP Confidence Score: {{ report.details.confidence_score if report.details and report.details.confidence_score is not none else '-' }} +- Visible Origins by Source: {{ report.details.visible_origins_by_source | tojson if report.details and report.details.visible_origins_by_source else '-' }} +- Conflicting Origins: {{ report.details.conflicting_origins | tojson if report.details and report.details.conflicting_origins else '[]' }} +- Required Actions: {{ report.details.required_actions | tojson if report.details and report.details.required_actions else '[]' }} +- Post-Change Verification Status: {{ report.details.post_change_status if report.details and report.details.post_change_status else '-' }} + ## 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 | diff --git a/backend/tests/test_api_smoke.py b/backend/tests/test_api_smoke.py index a57f4af..52475a4 100644 --- a/backend/tests/test_api_smoke.py +++ b/backend/tests/test_api_smoke.py @@ -563,3 +563,28 @@ def test_roa_preflight_suggested_roa_and_invalid_max_length() -> None: critical = client.post('/api/check/roa-preflight', json={'prefix':'203.0.113.0/24','origin_as':'AS3320','max_length':23}) assert critical.status_code == 200 assert critical.json()['status'] == 'CRITICAL' + +def test_change_case_workflow_endpoints_and_watch_alert_fields() -> None: + client = _client() + _setup_and_login(client) + created = client.post('/api/change-cases', json={'title': 'Workflow', 'description': 'x'}) + cid = created.json()['id'] + client.patch(f'/api/change-cases/{cid}', json={'description': 'with preflight data'}) + + # missing validation data + bad = client.post(f'/api/change-cases/{cid}/run-preflight') + assert bad.status_code == 400 + + +def test_system_status_alerts_no_secret_fields() -> None: + client = _client() + _setup_and_login(client) + resp = client.get('/api/system/status') + assert resp.status_code == 200 + payload = resp.json() + alerts = payload.get('alerts', {}) + assert 'webhook_enabled' in alerts + assert 'on_status_change_only' in alerts + assert 'max_retries' in alerts + assert 'timeout_seconds' in alerts + assert 'secret' not in ''.join(alerts.keys()).lower() diff --git a/docs/architecture/providers.md b/docs/architecture/providers.md new file mode 100644 index 0000000..90d17f6 --- /dev/null +++ b/docs/architecture/providers.md @@ -0,0 +1,14 @@ +# Provider Architecture (Read-Only) + +RouteForge provider integrations are read-only and only fetch/aggregate external routing and registry data. + +## RPKI +- Provider abstraction: RIPEstat, Routinator, local JSON, auto fallback. +- Diagnostics: fallback usage, disagreement flags, source diagnostics. + +## BGP Visibility +- Multi-source aggregation with provider-level diagnostics and confidence scoring. + +## Security Model +- No write operations against routers, RIPE DB, IRR, or ROA systems. +- No secrets in frontend, reports, logs, or audit details. diff --git a/docs/operations/bgp-visibility-providers.md b/docs/operations/bgp-visibility-providers.md new file mode 100644 index 0000000..266ebc8 --- /dev/null +++ b/docs/operations/bgp-visibility-providers.md @@ -0,0 +1,13 @@ +# BGP Visibility Providers + +- `BGP_VISIBILITY_PROVIDERS` supports comma-separated providers (currently `ripestat,generic-http`). +- `generic-http` uses `BGP_GENERIC_URL_TEMPLATE` with `{prefix}` interpolation. +- Multi-source output includes: + - provider/source list + - visible origins by source + - all visible origins + - source agreement + - confidence score + - failed provider count + - source diagnostics +- Conflicting origins and missing expected origin are exposed in reports/UI. diff --git a/docs/operations/change-case-workflow.md b/docs/operations/change-case-workflow.md new file mode 100644 index 0000000..b41dec3 --- /dev/null +++ b/docs/operations/change-case-workflow.md @@ -0,0 +1,11 @@ +# Change Case Workflow + +1. Create change case. +2. Populate `affected_prefixes` and `planned_origin_asns`. +3. Run `POST /api/change-cases/{id}/run-preflight`. +4. Review `decision`, `risk_summary`, and `required_actions`. +5. Execute routing change outside RouteForge (RouteForge is read-only). +6. Run `POST /api/change-cases/{id}/run-post-change-verification`. +7. Review `post_change_status` (`VERIFIED`, `PARTIAL`, `FAILED`, `UNKNOWN`) and detected issues. + +Decision values: `GO`, `CAUTION`, `NO-GO`, `UNKNOWN`. diff --git a/docs/operations/rpki-provider.md b/docs/operations/rpki-provider.md new file mode 100644 index 0000000..2062212 --- /dev/null +++ b/docs/operations/rpki-provider.md @@ -0,0 +1,8 @@ +# RPKI Provider Operations + +- `RPKI_PROVIDER`: `ripestat`, `routinator`, `local-json`, or `auto`. +- `auto` tries local provider first (`routinator`) and can fallback to RIPEstat if `RPKI_FALLBACK_TO_RIPESTAT=true`. +- `RPKI_ROUTINATOR_URL` points to a read-only Routinator API endpoint. +- `RPKI_LOCAL_JSON_PATH` is used by `local-json` mode and must contain ROA-like JSON entries. +- Runtime output includes provider, fallback flags, disagreement indicators, and matched ROAs. +- RouteForge remains read-only: no ROA creation/writes. diff --git a/docs/operations/watch-alerts.md b/docs/operations/watch-alerts.md new file mode 100644 index 0000000..b69d34e --- /dev/null +++ b/docs/operations/watch-alerts.md @@ -0,0 +1,15 @@ +# Watch Webhook Alerts + +- Alerts are controlled by: + - `ALERT_WEBHOOK_ENABLED` + - `ALERT_WEBHOOK_URL` + - `ALERT_ON_STATUS_CHANGE_ONLY` + - `ALERT_WEBHOOK_MAX_RETRIES` + - `ALERT_WEBHOOK_TIMEOUT_SECONDS` +- HMAC header (optional): `X-RouteForge-Signature: sha256=` over `timestamp.body`. +- Additional headers: + - `X-RouteForge-Event` + - `X-RouteForge-Timestamp` +- Payload includes watch identifiers, previous/current status, summary, report ID and timestamp. +- Watch run records expose `alert_delivery_status`, `alert_delivered_at`, and truncated `alert_error_message`. +- Secrets are never returned in status payloads, reports, or UI. diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 3709a71..6e38a6d 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -84,6 +84,8 @@ export const runAsnCheck = (asn: string, change_case_id?: number) => requestJson export const deleteChangeCase = (id: number) => requestJson<{ ok: boolean; detached_checks: number }>(apiUrl(`/api/change-cases/${id}`), { method: 'DELETE' }) export const runPrefixCheck = (prefix: string, origin_as?: string, change_case_id?: number) => checkPrefix(prefix, origin_as, change_case_id) export const runPreflightCheck = (prefix: string, planned_origin_as: string, change_case_id?: number) => checkPreflight(prefix, planned_origin_as, change_case_id) +export const runChangeCasePreflight = (id: number) => requestJson<{ change_case_id:number; decision:string; required_actions:string[]; risk_summary:string }>(apiUrl(`/api/change-cases/${id}/run-preflight`), { method: 'POST' }) +export const runChangeCasePostChangeVerification = (id: number) => requestJson<{ change_case_id:number; post_change_status:string; verification_summary:string; detected_issues:string[] }>(apiUrl(`/api/change-cases/${id}/run-post-change-verification`), { method: 'POST' }) export const runBgpVisibilityCheck = (prefix: string, expected_origin_as?: string, change_case_id?: number) => requestJson(apiUrl('/api/check/bgp-visibility'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prefix, expected_origin_as: expected_origin_as || null, change_case_id: change_case_id ?? null }) }) diff --git a/frontend/src/components/ChangeCasesView.tsx b/frontend/src/components/ChangeCasesView.tsx index b955a43..2990a1a 100644 --- a/frontend/src/components/ChangeCasesView.tsx +++ b/frontend/src/components/ChangeCasesView.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { ApiError, createChangeCase, deleteChangeCase, getChangeCaseReports, getReportHtml, getReportMarkdown, getReportSummary, listChangeCases, runAsnCheck, runBgpVisibilityCheck, runPrefixCheck, runPreflightCheck, runRoaPreflightCheck, updateChangeCase } from '../api' +import { ApiError, createChangeCase, deleteChangeCase, getChangeCaseReports, getReportHtml, getReportMarkdown, getReportSummary, listChangeCases, runAsnCheck, runBgpVisibilityCheck, runChangeCasePostChangeVerification, runChangeCasePreflight, runPrefixCheck, runPreflightCheck, runRoaPreflightCheck, updateChangeCase } from '../api' import type { ChangeCaseItem, UserRole } from '../types' import { StatusBadge } from './StatusBadge' @@ -74,6 +74,12 @@ export function ChangeCasesView({ role }: { role: UserRole }) { {canEdit && }

    {selected.description || '—'}

    +
    +
    Preflight Decision: {selected.decision || 'UNKNOWN'}
    +
    Risk Summary: {selected.risk_summary || 'Not available yet.'}
    +
    Post-Change Verification: {selected.post_change_status || 'Not run yet.'}
    +
    Required Actions: {(selected.required_actions && selected.required_actions.length > 0) ? selected.required_actions.join(' · ') : 'None recorded.'}
    +
    {canEdit && editing &&
    setEditTitle(e.target.value)} /> @@ -83,6 +89,8 @@ export function ChangeCasesView({ role }: { role: UserRole }) {
    } {canEdit &&
    + + {(statusActions[selected.status] || []).map((action)=>)} {selected.status === 'closed' &&

    Case is closed (read-only workflow state).

    }
    } diff --git a/frontend/src/components/ReportView.tsx b/frontend/src/components/ReportView.tsx index b1c84b5..dfc638a 100644 --- a/frontend/src/components/ReportView.tsx +++ b/frontend/src/components/ReportView.tsx @@ -35,6 +35,7 @@ export function ReportView({ report }: { report: CheckResponse }) { const hasReportId = Number.isFinite(reportId) && reportId > 0 const rpkiBatch = details.rpki_batch as { message?: string } | undefined const sourceDiagnostics = (Array.isArray(details.source_diagnostics) ? details.source_diagnostics : []) as SourceDiagnostic[] + const visibilityProviders = (Array.isArray((details as Record).sources) ? (details as Record).sources : []) as string[] const notify = (message: string) => { setCopyMessage(message) @@ -87,6 +88,17 @@ export function ReportView({ report }: { report: CheckResponse }) {
    {[['RPKI', rpki], ['Registry/IRR', registry], ['Routing Visibility', routingVisibility] as const].map(([title, item]) => item &&

    {title}

    {item.summary || '-'}

    )}
    +
    +

    Provider & Decision Details

    +
    RPKI Provider: {String((rpki?.raw as Record | undefined)?.provider ?? (details as Record).provider ?? '-')}
    +
    RPKI Fallback used: {String((rpki?.raw as Record | undefined)?.fallback_used ?? (details as Record).fallback_used ?? false)}
    +
    RPKI Provider disagreement: {String((rpki?.raw as Record | undefined)?.provider_disagreement ?? (details as Record).provider_disagreement ?? false)}
    +
    BGP Providers: {visibilityProviders.length ? visibilityProviders.join(', ') : '-'}
    +
    Source agreement: {String((details as Record).source_agreement ?? '-')}
    +
    Confidence score: {String((details as Record).confidence_score ?? '-')}
    +
    Failed provider count: {String((details as Record).failed_provider_count ?? '-')}
    +
    Required actions: {Array.isArray((details as Record).required_actions) ? ((details as Record).required_actions as string[]).join(' · ') : '-'}
    +
    {sourceDiagnostics.length > 0 &&

    Data Source Diagnostics

    {sourceDiagnostics.map((d, idx) => )}
    SourceEndpointStatusFreshnessAttemptsRetriesFallbackStale CacheDurationCacheAgeTTLMessage
    {d.name || '-'}{d.endpoint || '-'}{d.freshness || 'UNKNOWN'}{typeof d.attempts === 'number' ? d.attempts : '-'}{typeof d.retry_count === 'number' ? d.retry_count : '-'}{d.fallback_used === true ? 'yes' : d.fallback_used === false ? 'no' : '-'}{d.stale_cache_used === true ? 'yes' : d.stale_cache_used === false ? 'no' : '-'}{typeof d.duration_ms === 'number' ? `${d.duration_ms} ms` : '-'}{d.cached === true ? 'HIT' : d.cached === false ? 'MISS' : '-'}{typeof d.cache_age_seconds === 'number' ? formatDurationSeconds(d.cache_age_seconds) : 'Unknown'}{typeof d.cache_ttl_seconds === 'number' ? formatDurationSeconds(d.cache_ttl_seconds) : 'Unknown'}
    {d.message || '-'}
    {d.fallback_used === true &&
    Live request failed. RouteForge used cached data. The result may be outdated.
    }{d.status === 'RATE_LIMITED' &&
    Rate limited by data source.
    }
    } diff --git a/frontend/src/components/WatchModeView.tsx b/frontend/src/components/WatchModeView.tsx index dad1584..e30a16e 100644 --- a/frontend/src/components/WatchModeView.tsx +++ b/frontend/src/components/WatchModeView.tsx @@ -154,7 +154,7 @@ export function WatchModeView({ role }: { role: UserRole }) { }

    Runs History

    - {runs.length === 0 ?
    No runs yet.
    : {runs.map(r=>)}
    created_atprevious_statusstatuschangedsummaryreport_id
    {r.created_at}{r.previous_status ?? 'n/a'}{r.status}{String(r.changed)}{r.summary}{r.report_id ? {r.report_id} : 'n/a'}
    } + {runs.length === 0 ?
    No runs yet.
    : {runs.map(r=>)}
    created_atprevious_statusstatuschangedsummaryalert_deliveryalert_delivered_atalert_errorreport_id
    {r.created_at}{r.previous_status ?? 'n/a'}{r.status}{String(r.changed)}{r.summary}{r.alert_delivery_status ?? 'n/a'}{r.alert_delivered_at ?? 'n/a'}{(r.alert_error_message ?? 'n/a').slice(0, 120)}{r.report_id ? {r.report_id} : 'n/a'}
    } } diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 168f4ec..6f1e7cd 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -134,8 +134,8 @@ export type AuditLogEntry = { } export type ChangeCaseStatus = 'draft' | 'in_review' | 'approved' | 'closed' -export type ChangeCaseItem = { id:number; title:string; description?:string|null; status:ChangeCaseStatus; created_by_user_id?:number|null; created_at:string; updated_at:string } +export type ChangeCaseItem = { id:number; title:string; description?:string|null; status:ChangeCaseStatus; created_by_user_id?:number|null; created_at:string; updated_at:string; affected_prefixes?:string[]|null; planned_origin_asns?:string[]|null; risk_summary?:string|null; decision?:string|null; required_actions?:string[]|null; post_change_status?:string|null; last_preflight_at?:string|null; last_verification_at?:string|null } export type WatchTarget = { id:number; name:string; watch_type:string; prefix?:string|null; asn?:string|null; origin_as?:string|null; expected_origin_as?:string|null; max_length?:number|null; interval_minutes:number; is_active:boolean; change_case_id?:number|null; created_by_user_id?:number|null; last_run_at?:string|null; next_run_at?:string|null; last_status?:string|null; created_at:string; updated_at:string } -export type WatchRun = { id:number; watch_target_id:number; report_id?:number|null; previous_status?:string|null; status:string; changed:boolean; summary:string; created_at:string } +export type WatchRun = { id:number; watch_target_id:number; report_id?:number|null; previous_status?:string|null; status:string; changed:boolean; summary:string; alert_delivery_status?:string|null; alert_delivered_at?:string|null; alert_error_message?:string|null; created_at:string }