From 0a4561f385db0a277e38aeefe2f25f94b78d9b63 Mon Sep 17 00:00:00 2001 From: oliver Date: Tue, 15 Sep 2026 23:10:26 +0800 Subject: [PATCH] Remove legacy OClaw alarm WSS and fix key-alert rules table layout. Key alerts now deliver only via the DSH hub; Modal Body no longer flex-shrinks the rules rows to empty. Co-authored-by: Cursor --- .env.example | 3 - PROD_MIN_CHECKLIST.md | 2 +- netx_api/app_shutdown.py | 7 - netx_api/config.py | 2 - netx_api/integrations_router.py | 51 +---- netx_api/key_alert_forward.py | 75 +------ netx_api/metrics_router.py | 13 -- netx_api/oclaw_alarm_forwarder.py | 350 ------------------------------ netx_api/ops_tasks_service.py | 26 +-- netx_api/runtime_task_messages.py | 8 +- netx_api/ume_alarms_router.py | 4 - netx_api/ume_inventory_router.py | 4 - netx_api/ume_key_alert_router.py | 10 +- netx_api/ume_runtime.py | 51 +---- netx_api/ume_support.py | 8 - netx_api/ume_sync_router.py | 7 - netx_api/ume_token_router.py | 4 - tests/test_stability_hardening.py | 1 - web/WEB.md | 4 +- web/src/i18n/en.ts | 22 -- web/src/i18n/zh.ts | 22 -- web/src/index.css | 15 ++ web/src/pages/UmePage.tsx | 41 ---- web/src/types.ts | 29 +-- web/src/utils/runtimeMessages.ts | 6 +- 25 files changed, 36 insertions(+), 729 deletions(-) delete mode 100644 netx_api/oclaw_alarm_forwarder.py diff --git a/.env.example b/.env.example index 41ee727..545d844 100644 --- a/.env.example +++ b/.env.example @@ -13,9 +13,6 @@ NETX_PARSER_CONFIG=netx_api/config/parsers/zte_alarm_monitor_v1.yaml NETX_OCLAW_ANALYZE_URL=http://127.0.0.1:8787/admin/api/ops-ai/analyze-sync NETX_OCLAW_ANALYZE_TOKEN= NETX_OCLAW_HEALTH_URL=http://127.0.0.1:8787/admin/api/ops-ai/health -NETX_OCLAW_ALARM_WS_ENABLED=false -NETX_OCLAW_ALARM_WS_URL=ws://127.0.0.1:8787/ws/netx-bridge -# 告警 WSS 鉴权与 NETX_OCLAW_ANALYZE_TOKEN 共用(OClaw 侧 OCLAW_OPS_AI_SHARED_TOKEN) NETX_UME_BASE_URL=https://10.227.157.143:18014 NETX_UME_USERNAME= NETX_UME_PASSWORD= diff --git a/PROD_MIN_CHECKLIST.md b/PROD_MIN_CHECKLIST.md index cd0aa2a..f0d6f64 100644 --- a/PROD_MIN_CHECKLIST.md +++ b/PROD_MIN_CHECKLIST.md @@ -22,7 +22,7 @@ - `netx` liveness: `/health/live` - `netx` readiness: `/health/ready` - `netx` integrations: `/v1/integrations/status` -- Alert when `oclaw_bridge.status != up` for more than 2 polling cycles. +- Alert when `dsh_alarm_hub` has no subscribers while key-alert rules are enabled (optional). - Alert when `db.status != up` or `latency_ms` exceeds threshold. ## Operations diff --git a/netx_api/app_shutdown.py b/netx_api/app_shutdown.py index 0975a17..3570103 100644 --- a/netx_api/app_shutdown.py +++ b/netx_api/app_shutdown.py @@ -67,13 +67,6 @@ def shutdown_runtime(*, reason: str = "lifespan") -> None: except Exception: # noqa: BLE001 _log.exception("shutdown_ws_consumer failed") - try: - from .oclaw_alarm_forwarder import shutdown_oclaw_alarm_forwarder - - shutdown_oclaw_alarm_forwarder() - except Exception: # noqa: BLE001 - _log.exception("shutdown_oclaw_alarm_forwarder failed") - try: from .webcrt_session_registry import close_all_sessions diff --git a/netx_api/config.py b/netx_api/config.py index 3ee9c87..4d67ab7 100644 --- a/netx_api/config.py +++ b/netx_api/config.py @@ -20,8 +20,6 @@ class Settings(BaseSettings): oclaw_connect_timeout_sec: float = 15.0 oclaw_analyze_read_timeout_sec: float = 180.0 oclaw_health_timeout_sec: float = 8.0 - oclaw_alarm_ws_enabled: bool = False - oclaw_alarm_ws_url: str = "ws://127.0.0.1:8787/ws/netx-bridge" # UME RESTCONF integration ume_base_url: str = "" ume_username: str = "" diff --git a/netx_api/integrations_router.py b/netx_api/integrations_router.py index 360aad9..0d0cba5 100644 --- a/netx_api/integrations_router.py +++ b/netx_api/integrations_router.py @@ -12,7 +12,6 @@ from sqlalchemy.orm import Session from .config import settings from .db import get_db from .dsh_alarm_hub import dsh_alarm_ws_loop, hub_status -from .oclaw_alarm_forwarder import forwarder_status router = APIRouter(tags=["health"]) @@ -79,7 +78,7 @@ def health_ready(db: Session = Depends(get_db)) -> dict[str, Any]: @router.get("/v1/integrations/status") def integrations_status(db: Session = Depends(get_db)) -> dict: - """netx API + DB + oclaw bridge status.""" + """netx API + DB + DSH alarm hub status.""" netx_api = {"status": "up"} db_status: dict = {"status": "unknown"} @@ -90,56 +89,8 @@ def integrations_status(db: Session = Depends(get_db)) -> dict: except Exception as exc: db_status = {"status": "down", "error": str(exc)[:240]} - oclaw_status: dict = {"status": "unknown"} - fwd = forwarder_status() - if not bool(fwd.get("enabled")): - oclaw_status = { - "status": "unknown", - "mode": "ws", - "enabled": False, - "connected": False, - "error_kind": "disabled", - "error": "NETX_OCLAW_ALARM_WS_ENABLED=false or missing token/url", - "forwarder": fwd, - } - elif bool(fwd.get("paused")): - oclaw_status = { - "status": "unknown", - "mode": "ws", - "enabled": True, - "connected": False, - "error_kind": "paused", - "error": "oclaw_alarm_forwarder runtime task paused", - "forwarder": fwd, - } - elif bool(fwd.get("connected")): - oclaw_status = { - "status": "up", - "mode": "ws", - "enabled": True, - "connected": True, - "queue_size": int(fwd.get("queue_size") or 0), - "published_ok": int(fwd.get("published_ok") or 0), - "published_fail": int(fwd.get("published_fail") or 0), - "url": str(fwd.get("url") or ""), - "forwarder": fwd, - } - else: - oclaw_status = { - "status": "down", - "mode": "ws", - "enabled": True, - "connected": False, - "error_kind": "ws_disconnected", - "error": "oclaw netx-bridge WebSocket not connected", - "queue_size": int(fwd.get("queue_size") or 0), - "url": str(fwd.get("url") or ""), - "forwarder": fwd, - } - return { "netx_api": netx_api, "db": db_status, - "oclaw_bridge": oclaw_status, "dsh_alarm_hub": hub_status(), } diff --git a/netx_api/key_alert_forward.py b/netx_api/key_alert_forward.py index ce3ecab..d359454 100644 --- a/netx_api/key_alert_forward.py +++ b/netx_api/key_alert_forward.py @@ -7,10 +7,9 @@ from typing import Any from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session +from .dsh_alarm_hub import publish_alarm from .key_alert_matcher import match_key_alert_rule from .models import UmeInventoryNE, UmeKeyAlertForwardLog -from .oclaw_alarm_forwarder import enqueue_alarm_forward, is_forwarder_operational -from .dsh_alarm_hub import publish_alarm from .ume_sync_service import ( _derive_ne_id_from_alarm, _pick, @@ -82,6 +81,7 @@ def maybe_forward_key_alert( ) .first() ) + # Column name is historical (oclaw_ok); now means DSH hub delivery succeeded. if existing is not None and int(existing.oclaw_ok or 0) == 1: return False @@ -96,22 +96,10 @@ def maybe_forward_key_alert( payload["rule_key"] = str(rule.notification_id or "") hub_sent = publish_alarm(payload) - oclaw_queued = False - if is_forwarder_operational(): - oclaw_queued = bool(enqueue_alarm_forward(payload)) - if hub_sent <= 0 and not oclaw_queued: + if hub_sent <= 0: return False - # Hub-only delivery already reached DSH clients — mark ok for dedup. - # Oclaw path stays pending until the bridge records a result. - delivered_ok = hub_sent > 0 and not oclaw_queued - status = [] - if hub_sent > 0: - status.append(f"dsh_hub:{hub_sent}") - if oclaw_queued: - status.append("oclaw_queued") - status_text = ",".join(status) if status else "queued" - + status_text = f"dsh_hub:{hub_sent}" row = existing if row is None: row = UmeKeyAlertForwardLog( @@ -120,62 +108,21 @@ def maybe_forward_key_alert( rule_key=str(rule.notification_id or ""), notification_id=notification_id_from_norm(norm), forwarded_at=_utc_now_naive(), - oclaw_ok=1 if delivered_ok else 0, - error="" if delivered_ok else status_text, + oclaw_ok=1, + error="", ) db.add(row) else: row.notification_id = notification_id_from_norm(norm) row.rule_key = str(rule.notification_id or "") row.forwarded_at = _utc_now_naive() - row.oclaw_ok = 1 if delivered_ok else 0 - row.error = "" if delivered_ok else status_text + row.oclaw_ok = 1 + row.error = "" try: db.commit() except IntegrityError: db.rollback() + _log.debug("key_alert forward log race alarm_key=%s action=%s", alarm_key, act) + else: + _log.debug("key_alert forwarded via DSH hub=%s status=%s", hub_sent, status_text) return True - - -def record_forward_result( - *, - alarm_key: str, - action: str, - ok: bool, - error: str = "", - rule_key: str = "", -) -> None: - from .db import SessionLocal - - key = str(alarm_key or "").strip() - act = str(action or "").strip().lower() - if not key or not act: - return - db = SessionLocal() - try: - row = ( - db.query(UmeKeyAlertForwardLog) - .filter(UmeKeyAlertForwardLog.alarm_key == key, UmeKeyAlertForwardLog.action == act) - .first() - ) - if row is None: - row = UmeKeyAlertForwardLog( - alarm_key=key, - action=act, - rule_key=str(rule_key or "").strip(), - forwarded_at=_utc_now_naive(), - oclaw_ok=1 if ok else 0, - error="" if ok else str(error or "forward_failed")[:240], - ) - db.add(row) - else: - if rule_key and not str(row.rule_key or "").strip(): - row.rule_key = str(rule_key or "").strip() - row.forwarded_at = _utc_now_naive() - row.oclaw_ok = 1 if ok else 0 - row.error = "" if ok else str(error or "forward_failed")[:240] - db.commit() - except Exception: - db.rollback() - finally: - db.close() diff --git a/netx_api/metrics_router.py b/netx_api/metrics_router.py index 031269c..9de7b91 100644 --- a/netx_api/metrics_router.py +++ b/netx_api/metrics_router.py @@ -14,7 +14,6 @@ from .config import settings from .db import db_pool_status from .db_storage_metrics import collect_db_storage_metrics from .host_metrics import collect_host_metrics -from .oclaw_alarm_forwarder import forwarder_status router = APIRouter(tags=["metrics"]) @@ -30,7 +29,6 @@ def collect_runtime_metrics() -> dict[str, Any]: "db_storage": collect_db_storage_metrics(), "cli_budget": cli_budget_status(), "audit_queue": audit_queue_status(), - "oclaw_forwarder": forwarder_status(), "schedulers_inline": bool(getattr(settings, "run_inline_schedulers", True)), "host": collect_host_metrics(), } @@ -92,17 +90,6 @@ def _prom_lines(metrics: dict[str, Any]) -> str: for key in ("depth", "dropped", "maxsize"): if key in audit: lines.append(f"netx_audit_queue_{key} {audit[key]}") - fwd = metrics.get("oclaw_forwarder") or {} - for key, prom in ( - ("queue_size", "netx_oclaw_forwarder_queue_size"), - ("published_ok", "netx_oclaw_forwarder_published_ok"), - ("published_fail", "netx_oclaw_forwarder_published_fail"), - ("dropped", "netx_oclaw_forwarder_dropped"), - ("requeued", "netx_oclaw_forwarder_requeued"), - ("retry_exhausted", "netx_oclaw_forwarder_retry_exhausted"), - ): - if key in fwd: - lines.append(f"{prom} {int(fwd.get(key) or 0)}") sched = metrics.get("device_schedulers") or {} if "stale" in sched: lines.append(f'netx_device_schedulers_stale {1 if sched.get("stale") else 0}') diff --git a/netx_api/oclaw_alarm_forwarder.py b/netx_api/oclaw_alarm_forwarder.py deleted file mode 100644 index 2739115..0000000 --- a/netx_api/oclaw_alarm_forwarder.py +++ /dev/null @@ -1,350 +0,0 @@ -from __future__ import annotations - -import json -import logging -import queue -import threading -import time -from collections.abc import Callable -from datetime import datetime, timezone -from typing import Any - -import websocket - -from .config import settings -from .runtime_task_messages import fwd_state_code - -_log = logging.getLogger("netx.oclaw.alarm_forwarder") - - -def _normalize_bridge_error(exc: BaseException | str) -> str: - """Map low-level WS errors to stable fwd:* codes for UI i18n.""" - raw = str(exc or "").strip() - low = raw.lower() - name = type(exc).__name__ if isinstance(exc, BaseException) else "" - if ( - "10061" in raw - or "actively refused" in low - or "积极拒绝" in raw - or "connection refused" in low - or name == "ConnectionRefusedError" - ): - return fwd_state_code("connect_refused") - if "timed out" in low or "timeout" in low or name in {"TimeoutError", "socket.timeout"}: - return fwd_state_code("connect_timeout") - if "auth failed" in low or "auth-fail" in low or "invalid_token" in low: - return fwd_state_code("auth_failed") - if raw: - return raw[:200] - return fwd_state_code("disconnected") - - -_OUTBOUND_Q: "queue.Queue[dict[str, Any]] | None" = None -_Q_LOCK = threading.Lock() -_STOP_EVENT = threading.Event() -_THREAD: threading.Thread | None = None -_CONN_LOCK = threading.Lock() -_WS: Any | None = None -_CONNECTED = threading.Event() -_IS_PAUSED: Callable[[], bool] | None = None -_ON_STATUS: Callable[[str], None] | None = None -_STATS_LOCK = threading.Lock() -_STATS: dict[str, int] = { - "published_ok": 0, - "published_fail": 0, - "queued": 0, - "dropped": 0, - "requeued": 0, - "retry_exhausted": 0, -} - - -def _outbound_q() -> "queue.Queue[dict[str, Any]]": - global _OUTBOUND_Q - with _Q_LOCK: - if _OUTBOUND_Q is None: - maxsize = max(100, int(getattr(settings, "oclaw_forward_queue_max", 2000) or 2000)) - _OUTBOUND_Q = queue.Queue(maxsize=maxsize) - return _OUTBOUND_Q - - -def _utc_now_iso() -> str: - return datetime.now(timezone.utc).isoformat() - - -def _bridge_token() -> str: - return str(getattr(settings, "oclaw_analyze_token", "") or "").strip() - - -def _bridge_url() -> str: - return str(getattr(settings, "oclaw_alarm_ws_url", "") or "").strip() - - -def is_forwarder_enabled() -> bool: - if not bool(getattr(settings, "oclaw_alarm_ws_enabled", False)): - return False - return bool(_bridge_url()) and bool(_bridge_token()) - - -def configure_oclaw_alarm_forwarder( - *, - is_paused: Callable[[], bool] | None = None, - on_status: Callable[[str], None] | None = None, -) -> None: - global _IS_PAUSED, _ON_STATUS - _IS_PAUSED = is_paused - _ON_STATUS = on_status - - -def _forwarder_paused() -> bool: - if _IS_PAUSED is None: - return False - try: - return bool(_IS_PAUSED()) - except Exception: - return False - - -def is_forwarder_operational() -> bool: - return is_forwarder_enabled() and not _forwarder_paused() - - -def _notify_status(msg: str) -> None: - if _ON_STATUS is None: - return - try: - _ON_STATUS(str(msg or "")[:240]) - except Exception: - pass - - -def request_forwarder_reconnect() -> None: - with _CONN_LOCK: - ws = _WS - if ws is not None: - try: - ws.close() - except Exception: - pass - - -def enqueue_alarm_forward(payload: dict[str, Any]) -> bool: - if not is_forwarder_operational(): - return False - try: - _outbound_q().put_nowait(dict(payload)) - with _STATS_LOCK: - _STATS["queued"] = int(_STATS.get("queued", 0)) + 1 - return True - except queue.Full: - with _STATS_LOCK: - _STATS["dropped"] = int(_STATS.get("dropped", 0)) + 1 - _log.warning("oclaw alarm forward queue full; dropping alarm_key=%s", payload.get("alarm_key")) - return False - - -def _requeue_or_drop(payload: dict[str, Any], *, reason: str) -> None: - max_retries = max(0, int(getattr(settings, "oclaw_forward_max_retries", 3) or 3)) - item = dict(payload) - attempts = int(item.get("_fwd_attempts") or 0) + 1 - item["_fwd_attempts"] = attempts - if attempts > max_retries: - with _STATS_LOCK: - _STATS["retry_exhausted"] = int(_STATS.get("retry_exhausted", 0)) + 1 - _STATS["dropped"] = int(_STATS.get("dropped", 0)) + 1 - _log.warning( - "oclaw forward drop after retries alarm_key=%s attempts=%s reason=%s", - item.get("alarm_key"), - attempts, - reason[:80], - ) - return - try: - _outbound_q().put_nowait(item) - with _STATS_LOCK: - _STATS["requeued"] = int(_STATS.get("requeued", 0)) + 1 - except queue.Full: - with _STATS_LOCK: - _STATS["dropped"] = int(_STATS.get("dropped", 0)) + 1 - _log.warning( - "oclaw forward requeue full; dropping alarm_key=%s", - item.get("alarm_key"), - ) - - -def _send_auth(ws: Any) -> bool: - token = _bridge_token() - ws.send(json.dumps({"type": "auth", "token": token}, ensure_ascii=False)) - deadline = time.time() + 10.0 - while time.time() < deadline: - raw = ws.recv() - if not raw: - continue - try: - msg = json.loads(raw) - except Exception: - continue - if not isinstance(msg, dict): - continue - if str(msg.get("type") or "").strip() == "auth-ok": - return True - if str(msg.get("type") or "").strip() == "auth-fail": - return False - return False - - -def _dispatch_one(ws: Any, payload: dict[str, Any]) -> dict[str, Any]: - envelope = { - "type": "event", - "event": "netx.alarm", - "payload": payload, - } - ws.send(json.dumps(envelope, ensure_ascii=False, default=str)) - deadline = time.time() + 15.0 - alarm_key = str(payload.get("alarm_key") or "") - while time.time() < deadline: - raw = ws.recv() - if not raw: - continue - try: - msg = json.loads(raw) - except Exception: - continue - if not isinstance(msg, dict): - continue - if str(msg.get("type") or "").strip() == "pong": - continue - if str(msg.get("type") or "").strip() == "ack": - if alarm_key and str(msg.get("alarm_key") or "").strip() not in {"", alarm_key}: - continue - return msg - return {"type": "ack", "alarm_key": alarm_key, "ok": False, "error": "ack_timeout"} - - -def _run_loop() -> None: - global _WS - backoff_s = 2.0 - while not _STOP_EVENT.is_set(): - if not is_forwarder_enabled() or _forwarder_paused(): - _CONNECTED.clear() - if _forwarder_paused(): - _notify_status("fwd:paused") - else: - _notify_status("fwd:disabled") - time.sleep(2.0) - continue - url = _bridge_url() - ws = None - try: - _notify_status("fwd:connecting") - ws = websocket.create_connection(url, timeout=20) - ws.settimeout(30) - if not _send_auth(ws): - raise RuntimeError("oclaw netx-bridge auth failed") - with _CONN_LOCK: - _WS = ws - _CONNECTED.set() - backoff_s = 2.0 - _log.info("oclaw netx-bridge connected url=%s", url[:120]) - _notify_status("fwd:connected") - while not _STOP_EVENT.is_set(): - if _forwarder_paused(): - _notify_status("fwd:paused") - raise RuntimeError("forwarder paused") - if not is_forwarder_enabled(): - _notify_status("fwd:disabled") - raise RuntimeError("forwarder disabled") - try: - payload = _outbound_q().get(timeout=1.0) - except queue.Empty: - try: - ws.send(json.dumps({"type": "ping", "ts": _utc_now_iso()}, ensure_ascii=False)) - except Exception: - raise - continue - try: - ack = _dispatch_one(ws, payload) - ok = bool(ack.get("ok")) - err = str(ack.get("error") or "")[:240] - with _STATS_LOCK: - if ok: - _STATS["published_ok"] = int(_STATS.get("published_ok", 0)) + 1 - else: - _STATS["published_fail"] = int(_STATS.get("published_fail", 0)) + 1 - try: - from .key_alert_forward import record_forward_result - - record_forward_result( - alarm_key=str(payload.get("alarm_key") or ""), - action=str(payload.get("action") or ""), - ok=ok, - error=err, - rule_key=str(payload.get("rule_key") or ""), - ) - except Exception as rec_exc: - _log.warning("forward result record failed: %s", str(rec_exc)[:120]) - if not ok: - _log.warning( - "oclaw ack failed alarm_key=%s error=%s", - payload.get("alarm_key"), - str(ack.get("error") or "")[:120], - ) - # Soft ack failure: do not infinite-requeue; count as fail only. - except Exception as exc: - _log.warning("oclaw forward failed alarm_key=%s err=%s", payload.get("alarm_key"), str(exc)[:120]) - _requeue_or_drop(payload, reason=str(exc)[:120]) - raise - finally: - _outbound_q().task_done() - except Exception as exc: - _CONNECTED.clear() - err = _normalize_bridge_error(exc) - _log.warning("oclaw netx-bridge disconnected: %s", str(exc)[:200]) - _notify_status(err) - time.sleep(backoff_s) - backoff_s = min(backoff_s * 1.5, 60.0) - finally: - with _CONN_LOCK: - _WS = None - _CONNECTED.clear() - if ws is not None: - try: - ws.close() - except Exception: - pass - - -def start_oclaw_alarm_forwarder() -> threading.Thread | None: - global _THREAD - if _THREAD is not None and _THREAD.is_alive(): - return _THREAD - _STOP_EVENT.clear() - _THREAD = threading.Thread(target=_run_loop, name="oclaw-alarm-forwarder", daemon=True) - _THREAD.start() - return _THREAD - - -def shutdown_oclaw_alarm_forwarder() -> None: - _STOP_EVENT.set() - - -def forwarder_status() -> dict[str, Any]: - with _STATS_LOCK: - stats = dict(_STATS) - paused = _forwarder_paused() - enabled = is_forwarder_enabled() - return { - "enabled": enabled, - "operational": bool(enabled and not paused), - "paused": paused, - "connected": _CONNECTED.is_set(), - "queue_size": int(_outbound_q().qsize()), - "queue_max": max(100, int(getattr(settings, "oclaw_forward_queue_max", 2000) or 2000)), - "url": _bridge_url(), - "published_ok": int(stats.get("published_ok", 0)), - "published_fail": int(stats.get("published_fail", 0)), - "queued_total": int(stats.get("queued", 0)), - "dropped": int(stats.get("dropped", 0)), - "requeued": int(stats.get("requeued", 0)), - "retry_exhausted": int(stats.get("retry_exhausted", 0)), - } diff --git a/netx_api/ops_tasks_service.py b/netx_api/ops_tasks_service.py index 2fe494d..83d24aa 100644 --- a/netx_api/ops_tasks_service.py +++ b/netx_api/ops_tasks_service.py @@ -336,13 +336,6 @@ def _ume_runtime_items() -> list[dict[str, Any]]: from .ume_support import _list_runtime_tasks except Exception: return [] - fwd: dict[str, Any] | None = None - try: - from .oclaw_alarm_forwarder import forwarder_status - - fwd = forwarder_status() - except Exception: - fwd = None ws_conn: dict[str, Any] | None = None try: from .ume_alarm_ws import get_ws_connection_status @@ -356,24 +349,7 @@ def _ume_runtime_items() -> list[dict[str, Any]]: status = str(row.get("status") or "unknown") progress = str(row.get("interval_label") or "") detail = str(row.get("last_error") or "")[:240] - if task == "oclaw_alarm_forwarder" and isinstance(fwd, dict): - bits: list[str] = [] - if not bool(fwd.get("enabled")): - bits.append("disabled") - elif bool(fwd.get("paused")): - bits.append("paused") - elif bool(fwd.get("connected")): - bits.append("connected") - else: - bits.append("disconnected") - q = int(fwd.get("queue_size") or 0) - if q > 0: - bits.append(f"q={q}") - pub_ok = int(fwd.get("published_ok") or 0) - if pub_ok > 0: - bits.append(f"pub={pub_ok}") - progress = " · ".join([p for p in (progress, *bits) if p]) - elif task == "alarms_current_ws_consumer" and isinstance(ws_conn, dict): + if task == "alarms_current_ws_consumer" and isinstance(ws_conn, dict): state = str(ws_conn.get("state") or ws_conn.get("status") or "").strip() if state: progress = " · ".join([p for p in (progress, state) if p]) diff --git a/netx_api/runtime_task_messages.py b/netx_api/runtime_task_messages.py index 14d2df5..44b0468 100644 --- a/netx_api/runtime_task_messages.py +++ b/netx_api/runtime_task_messages.py @@ -8,17 +8,11 @@ RT_ALARMS_SYNC_IN_PROGRESS_SKIP = "rt:alarms_sync_in_progress_skip" RT_PULLING_INVENTORY = "rt:pulling_inventory" RT_PULLING_TOPOLOGY = "rt:pulling_topology" RT_UME_WS_DISABLED_NO_BASE_URL = "rt:ume_ws_disabled_no_base_url" -RT_OCLAW_FWD_DISABLED = "rt:oclaw_fwd_disabled" RT_RESUMED_SYNC_SOON = "rt:resumed_sync_soon" RT_RESUMED_WSS_RECONNECT = "rt:resumed_wss_reconnect" -RT_RESUMED_OCLAW_WSS_RECONNECT = "rt:resumed_oclaw_wss_reconnect" RT_RESUMED = "rt:resumed" RT_KEEPALIVE_FAILED = "rt:keepalive_failed" def ws_state_code(state: str) -> str: - return f"ws:{str(state or '').strip()}" - - -def fwd_state_code(state: str) -> str: - return f"fwd:{str(state or '').strip()}" + return f"ws:{str(state or '').strip()}" \ No newline at end of file diff --git a/netx_api/ume_alarms_router.py b/netx_api/ume_alarms_router.py index e4757b7..472d37b 100644 --- a/netx_api/ume_alarms_router.py +++ b/netx_api/ume_alarms_router.py @@ -34,10 +34,6 @@ from .models import ( UmeKeyAlertRule, UmeSyncJob, ) -from .oclaw_alarm_forwarder import ( - forwarder_status, - request_forwarder_reconnect, -) from .ume_alarm_ws import ( cancel_alarm_subscription_manual, clear_local_alarm_subscription_manual, diff --git a/netx_api/ume_inventory_router.py b/netx_api/ume_inventory_router.py index b98c113..0d2129a 100644 --- a/netx_api/ume_inventory_router.py +++ b/netx_api/ume_inventory_router.py @@ -34,10 +34,6 @@ from .models import ( UmeKeyAlertRule, UmeSyncJob, ) -from .oclaw_alarm_forwarder import ( - forwarder_status, - request_forwarder_reconnect, -) from .ume_alarm_ws import ( cancel_alarm_subscription_manual, clear_local_alarm_subscription_manual, diff --git a/netx_api/ume_key_alert_router.py b/netx_api/ume_key_alert_router.py index ffc333b..1ae4280 100644 --- a/netx_api/ume_key_alert_router.py +++ b/netx_api/ume_key_alert_router.py @@ -35,10 +35,6 @@ from .models import ( UmeSyncJob, ) from .dsh_alarm_hub import hub_status as dsh_alarm_hub_status -from .oclaw_alarm_forwarder import ( - forwarder_status, - request_forwarder_reconnect, -) from .ume_alarm_ws import ( cancel_alarm_subscription_manual, clear_local_alarm_subscription_manual, @@ -146,14 +142,12 @@ def ume_list_key_alert_rules( } for row in rows ] - fwd = forwarder_status() hub = dsh_alarm_hub_status() return { "items": items, "total": total, "page": page, "page_size": page_size, - "forwarder": fwd, "dsh_alarm_hub": hub, } @@ -183,8 +177,6 @@ def ume_key_alert_monitor( "page_size": int(base.get("page_size") or page_size), "config": get_key_alert_monitor_config(db), "dsh_alarm_hub": base.get("dsh_alarm_hub") or dsh_alarm_hub_status(), - # Legacy OClaw outbound bridge (optional; demoted in UI). - "forwarder": base.get("forwarder") or forwarder_status(), } @@ -345,6 +337,6 @@ def ume_list_notification_ids( for nid, cause in rows if str(nid or "").strip() ] - return {"items": items, "total": len(items), "forwarder": forwarder_status()} + return {"items": items, "total": len(items)} diff --git a/netx_api/ume_runtime.py b/netx_api/ume_runtime.py index 67ef009..66ffe14 100644 --- a/netx_api/ume_runtime.py +++ b/netx_api/ume_runtime.py @@ -4,7 +4,7 @@ Device collectors (config_sync / LLDP / ne_collect / port_traffic) run via ``sta (API inline by default; set ``NETX_RUN_INLINE_SCHEDULERS=false`` and run ``python -m netx_api.worker`` for a split process). API process also owns UME keepalive, alarm WSS, current-alarm/inventory sync loops, -and oclaw forwarder via ``start_api_sideband_threads``. +and DSH alarm hub via ``start_api_sideband_threads``. """ from __future__ import annotations @@ -29,7 +29,6 @@ from .ume_sync_topology import fail_stale_topology_running_jobs from .runtime_task_messages import ( RT_ALARMS_SYNC_IN_PROGRESS_SKIP, RT_KEEPALIVE_FAILED, - RT_OCLAW_FWD_DISABLED, RT_PULLING_ALARMS_CURRENT, RT_PULLING_INVENTORY, RT_PULLING_TOPOLOGY, @@ -37,12 +36,6 @@ from .runtime_task_messages import ( RT_UME_WS_DISABLED_NO_BASE_URL, RT_WSS_ACTIVE_SKIP_REST, ) -from .oclaw_alarm_forwarder import ( - configure_oclaw_alarm_forwarder, - forwarder_status, - is_forwarder_enabled, - start_oclaw_alarm_forwarder, -) _log = logging.getLogger("netx.ume.runtime") _schedule_log = logging.getLogger("netx.ume.schedule") @@ -420,48 +413,6 @@ def start_api_sideband_threads() -> None: last_run_at=datetime.now(timezone.utc), last_error=f"startup_thread_init_failed: {str(exc)[:180]}", ) - try: - def _fwd_on_status(msg: str) -> None: - paused = ume_support._runtime_is_paused("oclaw_alarm_forwarder") - fwd = forwarder_status() - if paused: - status = "paused" - elif not bool(fwd.get("enabled")): - status = "paused" - elif bool(fwd.get("connected")): - status = "running" - else: - status = "running" - ume_support._set_runtime_task( - "oclaw_alarm_forwarder", - status=status, - last_run_at=datetime.now(timezone.utc), - last_error=str(msg or "")[:240], - ) - - configure_oclaw_alarm_forwarder( - is_paused=lambda: ume_support._runtime_is_paused("oclaw_alarm_forwarder"), - on_status=_fwd_on_status, - ) - if is_forwarder_enabled(): - ume_support._set_runtime_task("oclaw_alarm_forwarder", status="running", last_error="") - else: - ume_support._set_runtime_task( - "oclaw_alarm_forwarder", - status="paused", - last_error=RT_OCLAW_FWD_DISABLED, - ) - t_fwd = start_oclaw_alarm_forwarder() - if t_fwd is not None: - _schedule_log.info("started thread %s alive=%s", t_fwd.name, t_fwd.is_alive()) - except Exception as exc: - _schedule_log.exception("startup: oclaw_alarm_forwarder thread init failed: %s", exc) - ume_support._set_runtime_task( - "oclaw_alarm_forwarder", - status="error", - last_run_at=datetime.now(timezone.utc), - last_error=f"startup_thread_init_failed: {str(exc)[:180]}", - ) # Dock already has topology but Fabric/World empty (or last apply partial): heal without waiting 24h. try: diff --git a/netx_api/ume_support.py b/netx_api/ume_support.py index bb910c8..05d7200 100644 --- a/netx_api/ume_support.py +++ b/netx_api/ume_support.py @@ -18,11 +18,9 @@ from .timeutil import utcnow_naive from .runtime_task_messages import ( RT_ALARMS_SYNC_IN_PROGRESS_SKIP, RT_KEEPALIVE_FAILED, - RT_OCLAW_FWD_DISABLED, RT_PULLING_ALARMS_CURRENT, RT_PULLING_INVENTORY, RT_RESUMED, - RT_RESUMED_OCLAW_WSS_RECONNECT, RT_RESUMED_SYNC_SOON, RT_RESUMED_WSS_RECONNECT, RT_STARTUP_ALARM_SYNC_BEFORE_WS, @@ -30,7 +28,6 @@ from .runtime_task_messages import ( RT_UME_WS_DISABLED_NO_BASE_URL, RT_WSS_ACTIVE_SKIP_REST, ) -from .oclaw_alarm_forwarder import is_forwarder_enabled from .ume_alarm_ws import ( begin_startup_alarm_sync_gate, complete_startup_alarm_sync_gate, @@ -64,7 +61,6 @@ _UME_RUNTIME_TASKS: dict[str, dict[str, Any]] = { "token_keepalive": {"task": "token_keepalive", "status": "init", "last_run_at": None, "last_error": ""}, "alarms_current_auto_sync": {"task": "alarms_current_auto_sync", "status": "init", "last_run_at": None, "last_error": ""}, "alarms_current_ws_consumer": {"task": "alarms_current_ws_consumer", "status": "init", "last_run_at": None, "last_error": ""}, - "oclaw_alarm_forwarder": {"task": "oclaw_alarm_forwarder", "status": "init", "last_run_at": None, "last_error": ""}, "inventory_auto_sync": {"task": "inventory_auto_sync", "status": "init", "last_run_at": None, "last_error": ""}, "topology_auto_sync": {"task": "topology_auto_sync", "status": "init", "last_run_at": None, "last_error": ""}, } @@ -175,10 +171,6 @@ def _runtime_task_interval_fields(task_id: str) -> tuple[int | None, str]: if not bool(getattr(settings, "ume_alarm_ws_enabled", True)): return None, "disabled" return None, "realtime" - if task_id == "oclaw_alarm_forwarder": - if not is_forwarder_enabled(): - return None, "disabled" - return None, "realtime" if task_id == "inventory_auto_sync": if not bool(getattr(settings, "ume_sync_inventory_auto_enabled", True)): return None, "disabled" diff --git a/netx_api/ume_sync_router.py b/netx_api/ume_sync_router.py index 41d1cfc..1549f36 100644 --- a/netx_api/ume_sync_router.py +++ b/netx_api/ume_sync_router.py @@ -11,10 +11,8 @@ from sqlalchemy.orm import Session from .db import get_db from .models import UmeSyncJob -from .oclaw_alarm_forwarder import request_forwarder_reconnect from .runtime_task_messages import ( RT_RESUMED, - RT_RESUMED_OCLAW_WSS_RECONNECT, RT_RESUMED_SYNC_SOON, RT_RESUMED_WSS_RECONNECT, ) @@ -236,8 +234,6 @@ def ume_runtime_task_pause(task: str) -> dict[str, Any]: _clear_force_resume_hints(tid) if tid == "alarms_current_ws_consumer": request_ws_reconnect() - if tid == "oclaw_alarm_forwarder": - request_forwarder_reconnect() _set_runtime_task(tid, status="paused", last_error="") return {"ok": True, "task": tid, "runtime_tasks": _list_runtime_tasks()} @@ -254,9 +250,6 @@ def ume_runtime_task_resume(task: str) -> dict[str, Any]: elif tid == "alarms_current_ws_consumer": request_ws_reconnect() resume_hint = RT_RESUMED_WSS_RECONNECT - elif tid == "oclaw_alarm_forwarder": - request_forwarder_reconnect() - resume_hint = RT_RESUMED_OCLAW_WSS_RECONNECT else: resume_hint = RT_RESUMED _set_runtime_task(tid, status="running", last_error=resume_hint) diff --git a/netx_api/ume_token_router.py b/netx_api/ume_token_router.py index b3daec8..0fedc74 100644 --- a/netx_api/ume_token_router.py +++ b/netx_api/ume_token_router.py @@ -34,10 +34,6 @@ from .models import ( UmeKeyAlertRule, UmeSyncJob, ) -from .oclaw_alarm_forwarder import ( - forwarder_status, - request_forwarder_reconnect, -) from .ume_alarm_ws import ( cancel_alarm_subscription_manual, clear_local_alarm_subscription_manual, diff --git a/tests/test_stability_hardening.py b/tests/test_stability_hardening.py index f608001..eefb1fb 100644 --- a/tests/test_stability_hardening.py +++ b/tests/test_stability_hardening.py @@ -71,7 +71,6 @@ class StabilityHardeningTests(unittest.TestCase): self.assertIn("netx_uptime_seconds", body) self.assertIn("netx_thread_count", body) self.assertIn("netx_cli_budget_limit", body) - self.assertIn("netx_oclaw_forwarder_dropped", body) self.assertIn("netx_device_schedulers_stale", body) def test_log_runtime_budget_does_not_raise(self) -> None: diff --git a/web/WEB.md b/web/WEB.md index c030633..1662e18 100644 --- a/web/WEB.md +++ b/web/WEB.md @@ -80,8 +80,8 @@ src/ - API 仅写在 `services/api.ts` - Query key 集中在 `constants/queryKeys.ts` - 失效缓存用 prefix key(如 `queryKeys.umeSyncStatusAll`) -- 顶栏连接状态:`App` 轮询 `GET /v1/integrations/status`(5s),展示 **netx api** 与 **oclaw bridge**(含延迟 / 错误类型) -- 关键告警推送主路径:NetX **DSH alarm hub**(`/v1/integrations/dsh-alarm/ws`)为服务器,Netx Ops 外拨订阅;UME 页展示多订阅连接列表。OClaw 出站 WSS 为遗留旁路(`NETX_OCLAW_ALARM_WS_ENABLED`) +- 顶栏连接状态:`App` 轮询 `GET /v1/integrations/status`(5s),展示 **netx api** 与 **DSH alarm hub** +- 关键告警推送:NetX **DSH alarm hub**(`/v1/integrations/dsh-alarm/ws`)为服务器,Netx Ops 外拨订阅;UME 页展示多订阅连接列表 ## 网元管理(独立于 UME) diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 19e4772..c1e54b8 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1087,15 +1087,6 @@ const en = { hubColLastSeen: "last seen", hubEmptyConnections: "No Netx Ops clients are connected to the alarm hub", hubPath: "path", - legacyOclaw: "Legacy OClaw outbound", - ws: "OClaw WSS", - wsConnected: "connected", - wsDisconnected: "disconnected", - wsDisabled: "disabled", - wsPaused: "paused", - publishedOk: "published ok", - publishedFail: "published fail", - queue: "queue", matchType: "Match by", matchNotificationId: "notificationId", matchKeyword: "description keyword", @@ -1171,10 +1162,8 @@ const en = { pulling_topology: "Pulling UME topology (nodes/links)…", topology_sync_busy: "Topology sync already running; skipped", ume_ws_disabled_no_base_url: "Disabled or UME_BASE_URL not configured", - oclaw_fwd_disabled: "Disabled or NETX_OCLAW_ALARM_WS / token / url not configured", resumed_sync_soon: "Resumed: skipping debounce wait, sync soon", resumed_wss_reconnect: "Resumed: reconnecting WSS soon", - resumed_oclaw_wss_reconnect: "Resumed: reconnecting OClaw WSS soon", resumed: "Resumed", keepalive_failed: "Token keepalive failed", }, @@ -1191,21 +1180,10 @@ const en = { subscription_lost: "UME subscription lost", disabled_no_base_url: "Disabled or UME_BASE_URL not configured", }, - fwdState: { - paused: "Paused", - disabled: "Disabled", - connecting: "Connecting", - connected: "Connected", - connect_refused: "OClaw gateway down or port 8787 refused", - connect_timeout: "Timed out connecting to OClaw gateway", - auth_failed: "OClaw netx-bridge auth failed (check shared token)", - disconnected: "OClaw WSS disconnected", - }, runtimeTask: { token_keepalive: "UME token keepalive", alarms_current_auto_sync: "Current alarms auto sync", alarms_current_ws_consumer: "UME alarm WSS", - oclaw_alarm_forwarder: "OClaw key-alert WSS", inventory_auto_sync: "Inventory auto sync", topology_auto_sync: "Topology auto sync", }, diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index c5b98d0..d792e3c 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -1079,15 +1079,6 @@ const zh = { hubColLastSeen: "最近心跳", hubEmptyConnections: "当前没有 Netx Ops 客户端连入告警 hub", hubPath: "路径", - legacyOclaw: "遗留 OClaw 出站", - ws: "OClaw WSS", - wsConnected: "已连接", - wsDisconnected: "未连接", - wsDisabled: "未启用", - wsPaused: "已暂停", - publishedOk: "发布成功", - publishedFail: "发布失败", - queue: "待发队列", matchType: "匹配方式", matchNotificationId: "告警码 (notificationId)", matchKeyword: "告警描述关键字", @@ -1163,10 +1154,8 @@ const zh = { pulling_topology: "正在拉取 UME 拓扑(节点/链路)…", topology_sync_busy: "拓扑同步进行中,已跳过", ume_ws_disabled_no_base_url: "未启用或未配置 UME_BASE_URL", - oclaw_fwd_disabled: "未启用或未配置 NETX_OCLAW_ALARM_WS / token / url", resumed_sync_soon: "已恢复:将跳过本轮周期等待并尽快同步", resumed_wss_reconnect: "已恢复:将尽快重连 WSS", - resumed_oclaw_wss_reconnect: "已恢复:将尽快重连 OClaw WSS", resumed: "已恢复", keepalive_failed: "Token 保活失败", }, @@ -1183,21 +1172,10 @@ const zh = { subscription_lost: "UME 订阅已丢失", disabled_no_base_url: "未启用或未配置 UME_BASE_URL", }, - fwdState: { - paused: "已暂停", - disabled: "未启用", - connecting: "连接中", - connected: "已连接", - connect_refused: "OClaw 网关未启动或 8787 拒绝连接", - connect_timeout: "连接 OClaw 网关超时", - auth_failed: "OClaw netx-bridge 鉴权失败(检查共享 token)", - disconnected: "OClaw WSS 已断开", - }, runtimeTask: { token_keepalive: "UME Token 保活", alarms_current_auto_sync: "当前告警定时同步", alarms_current_ws_consumer: "UME 告警 WSS", - oclaw_alarm_forwarder: "OClaw 关键告警 WSS", inventory_auto_sync: "Inventory 定时同步", topology_auto_sync: "拓扑定时同步", }, diff --git a/web/src/index.css b/web/src/index.css index d1970b9..132f30d 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -11312,10 +11312,25 @@ option { max-height: min(78vh, 780px); } +/* + * Modal.Body often uses flex column; default flex-shrink:1 crushes the rules + * table to thead-only while the pager still shows total > 0. + */ +.app-heroui-modal__dialog [data-slot="modal-body"] > *, +.app-heroui-modal__dialog .modal__body > * { + flex-shrink: 0; +} + +.app-heroui-modal--xl .app-heroui-modal__dialog [data-slot="modal-body"], +.app-heroui-modal--xl .app-heroui-modal__dialog .modal__body { + max-height: min(86vh, 900px); +} + .app-heroui-modal .pt-list-table-wrap { overflow-x: auto; max-width: 100%; width: 100%; + min-height: 10rem; } .app-heroui-modal .pt-list-table { diff --git a/web/src/pages/UmePage.tsx b/web/src/pages/UmePage.tsx index 6c80fb0..100b75d 100644 --- a/web/src/pages/UmePage.tsx +++ b/web/src/pages/UmePage.tsx @@ -437,21 +437,11 @@ export function UmePage() { const keyAlertHub = keyAlertMonitorQuery.data?.dsh_alarm_hub; const keyAlertHubConnections = keyAlertHub?.connections || []; const keyAlertHubSubscribers = Number(keyAlertHub?.subscribers || keyAlertHubConnections.length || 0); - const keyAlertForwarder = keyAlertMonitorQuery.data?.forwarder; const keyAlertRules = keyAlertMonitorQuery.data?.rules || []; const keyAlertTotal = Number(keyAlertMonitorQuery.data?.total || keyAlertRules.length); const keyAlertPages = pageCount(keyAlertTotal, keyAlertPageSize); const keyAlertForwardOnClear = Boolean(keyAlertMonitorQuery.data?.config?.forward_on_clear); const hubWsPill = keyAlertHubSubscribers > 0 ? "up" : "down"; - const oclawWsPill = - !keyAlertForwarder?.enabled - ? "unknown" - : keyAlertForwarder.paused - ? "warn" - : keyAlertForwarder.connected - ? "up" - : "down"; - const showLegacyOclaw = Boolean(keyAlertForwarder?.enabled); const runtimeTaskLabel = (task: string) => { const key = `ume.tasks.runtimeTask.${task}`; @@ -878,16 +868,6 @@ export function UmePage() { {t("ume.keyAlert.hubDeliverOk")}: {Number(keyAlertHub?.deliver_ok || 0)} - {showLegacyOclaw ? ( - - {t("ume.keyAlert.ws")}:{" "} - {keyAlertForwarder?.paused - ? t("ume.keyAlert.wsPaused") - : keyAlertForwarder?.connected - ? t("ume.keyAlert.wsConnected") - : t("ume.keyAlert.wsDisconnected")} - - ) : null} @@ -984,15 +964,6 @@ export function UmePage() { {` · ${t("ume.keyAlert.hubPublished")} ${Number(keyAlertHub?.published || 0)}`} {` · ${t("ume.keyAlert.hubDeliverOk")} ${Number(keyAlertHub?.deliver_ok || 0)}`} {keyAlertHub?.path ? ` · ${t("ume.keyAlert.hubPath")} ${keyAlertHub.path}` : ""} - {showLegacyOclaw - ? ` · ${t("ume.keyAlert.legacyOclaw")}: ${ - keyAlertForwarder?.paused - ? t("ume.keyAlert.wsPaused") - : keyAlertForwarder?.connected - ? t("ume.keyAlert.wsConnected") - : t("ume.keyAlert.wsDisconnected") - }` - : ""}

{t("ume.keyAlert.hubConnections")}
@@ -1036,18 +1007,6 @@ export function UmePage() {
)} - {showLegacyOclaw ? ( -

- {t("ume.keyAlert.legacyOclaw")}:{" "} - {keyAlertForwarder?.paused - ? t("ume.keyAlert.wsPaused") - : keyAlertForwarder?.connected - ? t("ume.keyAlert.wsConnected") - : t("ume.keyAlert.wsDisconnected")} - {` · ${t("ume.keyAlert.publishedOk")} ${Number(keyAlertForwarder?.published_ok || 0)}`} - {` · ${t("ume.keyAlert.queue")} ${Number(keyAlertForwarder?.queue_size || 0)}`} -

- ) : null}
diff --git a/web/src/types.ts b/web/src/types.ts index 2937f8e..aabeee5 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -21,19 +21,6 @@ export type IntegrationStatus = { netx_api: { status: "up" | "down" | "unknown"; [k: string]: unknown }; db: { status: "up" | "down" | "unknown"; latency_ms?: number; error?: string; [k: string]: unknown }; dsh_alarm_hub?: DshAlarmHubStatus; - oclaw_bridge?: { - status: "up" | "down" | "unknown"; - mode?: string; - enabled?: boolean; - connected?: boolean; - queue_size?: number; - published_ok?: number; - published_fail?: number; - latency_ms?: number; - error_kind?: string; - error?: string; - [k: string]: unknown; - }; }; export type UmeKeyAlertRuleItem = { @@ -57,18 +44,6 @@ export type UmeInventoryNeTypeItem = { ne_count: number; }; -export type UmeKeyAlertForwarderStatus = { - enabled: boolean; - operational?: boolean; - paused?: boolean; - connected: boolean; - queue_size: number; - url: string; - published_ok?: number; - published_fail?: number; - queued_total?: number; -}; - export type UmeKeyAlertMonitorResponse = { ok: boolean; rules: UmeKeyAlertRuleItem[]; @@ -78,10 +53,8 @@ export type UmeKeyAlertMonitorResponse = { config?: { forward_on_clear: boolean; }; - /** Primary: NetX hub ← netxops clients (multi-subscriber). */ + /** NetX hub ← netxops clients (multi-subscriber). */ dsh_alarm_hub?: DshAlarmHubStatus; - /** Legacy: NetX → OClaw outbound bridge (single link). */ - forwarder: UmeKeyAlertForwarderStatus; }; export type UmeSyncJobItem = { diff --git a/web/src/utils/runtimeMessages.ts b/web/src/utils/runtimeMessages.ts index f24346a..add2bd5 100644 --- a/web/src/utils/runtimeMessages.ts +++ b/web/src/utils/runtimeMessages.ts @@ -1,4 +1,4 @@ -/** Map backend runtime status codes (rt:* / ws:* / fwd:*) and legacy Chinese to i18n keys. */ +/** Map backend runtime status codes (rt:* / ws:*) and legacy Chinese to i18n keys. */ type TFunc = (key: string) => string; @@ -11,10 +11,8 @@ const LEGACY_ZH_RUNTIME_ERROR: Record = { "正在拉取 UME 网元清单…": "ume.tasks.runtimeError.pulling_inventory", "正在拉取 UME 拓扑(节点/链路)…": "ume.tasks.runtimeError.pulling_topology", "未启用或未配置 UME_BASE_URL": "ume.tasks.runtimeError.ume_ws_disabled_no_base_url", - "未启用或未配置 NETX_OCLAW_ALARM_WS / token / url": "ume.tasks.runtimeError.oclaw_fwd_disabled", "已恢复:将跳过本轮周期等待并尽快同步": "ume.tasks.runtimeError.resumed_sync_soon", "已恢复:将尽快重连 WSS": "ume.tasks.runtimeError.resumed_wss_reconnect", - "已恢复:将尽快重连 OClaw WSS": "ume.tasks.runtimeError.resumed_oclaw_wss_reconnect", 已恢复: "ume.tasks.runtimeError.resumed", 初始化: "ume.tasks.wsState.init", 已连接: "ume.tasks.wsState.connected", @@ -45,8 +43,6 @@ export function runtimeLastError(raw: string | null | undefined, t: TFunc): stri if (rt) return rt; const ws = translatePrefixed(s, "ws:", "ume.tasks.wsState", t); if (ws) return ws; - const fwd = translatePrefixed(s, "fwd:", "ume.tasks.fwdState", t); - if (fwd) return fwd; if (s === "keepalive_failed") { const tr = t("ume.tasks.runtimeError.keepalive_failed");