diff --git a/CHANGELOG.md b/CHANGELOG.md index c72f76c1..cc639f6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.481] — 2026-06-17 — Release QP (gateway start/stop/restart controls in Settings) + +### Added + +- **Settings → System now has start / stop / restart controls for the messaging gateway (#3628).** Manage the Hermes gateway (Telegram, Discord, Slack, etc.) from the WebUI without dropping to a terminal. The controls call the existing platform-aware `hermes gateway {start,stop,restart}` CLI (systemd on Linux, launchd on macOS), refresh the gateway status card, and surface bounded errors. The endpoint is auth + CSRF gated, validates the active profile name, runs a list-argv subprocess (no shell), and is serialized server-side so overlapping actions return 409 rather than spawning concurrent gateway processes. Thanks @rodboev. + ## [v0.51.480] — 2026-06-17 — Release QO (inline PDF preview renders all pages) ### Fixed diff --git a/api/routes.py b/api/routes.py index 37b1d0e7..e94a2c76 100644 --- a/api/routes.py +++ b/api/routes.py @@ -949,6 +949,206 @@ def _load_gateway_session_identity_map() -> dict[str, dict]: return mapping.copy() +def _gateway_status_payload() -> dict: + import datetime + + identity_map = _load_gateway_session_identity_map() + sessions_path = _gateway_session_metadata_path() + + # Detect whether the gateway process is alive, independent of connected + # messaging platforms. An empty identity_map means zero connected + # platforms, not necessarily a stopped gateway. + health = build_agent_health_payload() + alive = health.get("alive") + details = health.get("details") if isinstance(health.get("details"), dict) else {} + health_reason = details.get("reason") + health_state = details.get("state") + health_gateway_state = details.get("gateway_state") + if alive is True: + running = True + configured = True + elif alive is False: + running = False + configured = True + else: + gateway_running_metadata = ( + health_reason == "gateway_stale_running_state" + or health_gateway_state == "running" + ) + configured = True if gateway_running_metadata else bool(identity_map) + running = bool(identity_map) + + platforms_set: set[str] = set() + for meta in identity_map.values(): + raw = meta.get("raw_source") or meta.get("platform") or "" + norm = _normalize_messaging_source(raw) + if norm: + platforms_set.add(norm) + platform_labels = { + "telegram": "Telegram", + "discord": "Discord", + "slack": "Slack", + "email": "Email", + "web": "Web", + "api": "API", + } + platforms = sorted( + [{"name": p, "label": platform_labels.get(p, p.title())} for p in platforms_set], + key=lambda x: x["label"], + ) + last_active = "" + if running and sessions_path.exists(): + try: + mtime = sessions_path.stat().st_mtime + last_active = datetime.datetime.fromtimestamp(mtime).isoformat() + except Exception: + pass + return { + "running": running, + "configured": configured, + "platforms": platforms, + "last_active": last_active, + "session_count": len(identity_map), + "health": { + "state": health_state, + "reason": health_reason, + "gateway_state": health_gateway_state, + }, + } + + +_GATEWAY_LIFECYCLE_TIMEOUT_SECONDS = 60 + +# Server-side single-flight guard for gateway lifecycle actions. The client +# disables its button while a request is in flight, but a scripted authed +# client could still fire overlapping start/stop/restart calls, spawning +# concurrent `hermes gateway` subprocesses. Serialize them here (mirrors the +# self-update _apply_lock pattern): a non-blocking acquire returns 409 on +# contention rather than launching a second overlapping subprocess. +_GATEWAY_ACTION_LOCK = threading.Lock() + + +def _run_gateway_lifecycle_command(action: str) -> subprocess.CompletedProcess: + if action not in {"start", "stop", "restart"}: + raise ValueError("unsupported gateway action") + + from api import config as api_config + from api.profiles import get_active_profile_name + + agent_dir = getattr(api_config, "_AGENT_DIR", None) + if not agent_dir: + raise FileNotFoundError("Hermes agent checkout not found") + agent_dir = Path(agent_dir).expanduser().resolve() + main_py = agent_dir / "hermes_cli" / "main.py" + if not main_py.exists(): + raise FileNotFoundError("Hermes agent CLI entrypoint not found") + + cmd = [str(getattr(api_config, "PYTHON_EXE", sys.executable)), str(main_py)] + profile_name = "" + try: + profile_name = str(get_active_profile_name() or "").strip() + except Exception as exc: + logger.debug("Could not resolve active profile for gateway lifecycle: %s", exc) + if profile_name and profile_name != "default": + cmd.extend(["--profile", profile_name]) + cmd.extend(["gateway", action]) + + env = os.environ.copy() + env.setdefault("PYTHONUTF8", "1") + env.setdefault("BROWSER", "echo") + return subprocess.run( + cmd, + cwd=str(agent_dir), + env=env, + capture_output=True, + text=True, + timeout=_GATEWAY_LIFECYCLE_TIMEOUT_SECONDS, + ) + + +def _handle_gateway_lifecycle(handler, action: str, body: dict): + del body # Reserved for future per-gateway naming without changing the route contract. + # Reject overlapping lifecycle actions instead of spawning concurrent + # `hermes gateway` subprocesses (a non-blocking acquire — the action holds + # the lock for at most _GATEWAY_LIFECYCLE_TIMEOUT_SECONDS). + if action not in {"start", "stop", "restart"}: + return bad(handler, "unsupported gateway action", 400) + if not _GATEWAY_ACTION_LOCK.acquire(blocking=False): + return j( + handler, + { + "ok": False, + "error": "Another gateway action is already in progress; try again shortly.", + "action": action, + }, + status=409, + ) + try: + result = _run_gateway_lifecycle_command(action) + except ValueError as exc: + return bad(handler, str(exc), 400) + except FileNotFoundError as exc: + return j(handler, {"ok": False, "error": _sanitize_error(exc), "action": action}, status=500) + except subprocess.TimeoutExpired as exc: + logger.warning( + "Gateway %s command timed out after %ss; stdout=%r stderr=%r", + action, + _GATEWAY_LIFECYCLE_TIMEOUT_SECONDS, + exc.stdout, + exc.stderr, + ) + return j( + handler, + { + "ok": False, + "error": f"Gateway {action} timed out after {_GATEWAY_LIFECYCLE_TIMEOUT_SECONDS} seconds", + "action": action, + }, + status=504, + ) + except Exception as exc: + logger.exception("Gateway %s command failed before completion", action) + return j(handler, {"ok": False, "error": _sanitize_error(exc), "action": action}, status=500) + finally: + _GATEWAY_ACTION_LOCK.release() + + stdout = (result.stdout or "").strip() + stderr = (result.stderr or "").strip() + if result.returncode != 0: + logger.warning( + "Gateway %s command failed with exit code %s; stdout=%r stderr=%r", + action, + result.returncode, + stdout, + stderr, + ) + return j( + handler, + { + "ok": False, + "error": f"Gateway {action} failed with exit code {result.returncode}", + "action": action, + "returncode": result.returncode, + }, + status=500, + ) + + return j( + handler, + { + "ok": True, + "action": action, + # Do NOT return captured stdout/stderr — the `hermes gateway` CLI + # prints service/PID/status details the browser shouldn't receive + # (mirrors the failure path, which already suppresses them). The + # frontend localizes its own success copy; the refreshed status + # payload carries the user-facing state. + "message": f"Gateway {action} completed.", + "status": _gateway_status_payload(), + }, + ) + + def _mark_cron_running(job_id: str): with _RUNNING_CRON_LOCK: _RUNNING_CRON_JOBS[job_id] = time.time() @@ -7977,97 +8177,7 @@ def handle_get(handler, parsed) -> bool: # ── Gateway Status (GET) ── if parsed.path == "/api/gateway/status": - import datetime - identity_map = _load_gateway_session_identity_map() - sessions_path = _gateway_session_metadata_path() - - # Detect whether the gateway process is alive, independent of - # connected messaging platforms. An empty identity_map just - # means zero platforms connected, not that the gateway is down. - # - # agent_health.build_agent_health_payload() is the authoritative - # signal: it reads gateway.status runtime metadata and returns a - # tri-state `alive` field (True/False/None). This avoids the - # false-negative where the gateway is running but has zero active - # messaging sessions (empty identity_map). - # - # `alive` tri-state semantics: - # True → gateway process is alive - # False → gateway metadata exists but process is down - # None → no gateway metadata/status available; this WebUI - # setup is probably not configured with a gateway - health = build_agent_health_payload() - alive = health.get("alive") - details = health.get("details") if isinstance(health.get("details"), dict) else {} - health_reason = details.get("reason") - health_state = details.get("state") - health_gateway_state = details.get("gateway_state") - if alive is True: - running = True - configured = True - elif alive is False: - running = False - configured = True - else: # alive is None - # `alive is None` conflates two very different states: - # (a) no gateway metadata at all → genuinely not configured - # (b) gateway metadata EXISTS but is stale/inconclusive. - # A stale-but-RUNNING gateway (freshly started, hasn't ticked - # `updated_at` yet, or cross-container) still proves the gateway is - # *configured* — the banner must not report "Gateway not configured" - # just because no conversation has happened yet and identity_map is - # empty (#3194). - # - # A stale-STOPPED gateway is deliberately NOT treated as configured: - # agent_health emits `gateway_stale_stopped_state` precisely so a - # stopped service the user isn't running reads like "no root gateway - # configured" rather than nagging (#1944). So stale-stopped falls - # through to the identity_map signal like the genuinely-unconfigured - # case. - gateway_running_metadata = ( - health_reason == "gateway_stale_running_state" - or health_gateway_state == "running" - ) - configured = True if gateway_running_metadata else bool(identity_map) - running = bool(identity_map) - - platforms_set: set[str] = set() - for meta in identity_map.values(): - raw = meta.get("raw_source") or meta.get("platform") or "" - norm = _normalize_messaging_source(raw) - if norm: - platforms_set.add(norm) - _PLATFORM_LABELS = { - "telegram": "Telegram", - "discord": "Discord", - "slack": "Slack", - "email": "Email", - "web": "Web", - "api": "API", - } - platforms = sorted( - [{"name": p, "label": _PLATFORM_LABELS.get(p, p.title())} for p in platforms_set], - key=lambda x: x["label"], - ) - last_active = "" - if running and sessions_path.exists(): - try: - mtime = sessions_path.stat().st_mtime - last_active = datetime.datetime.fromtimestamp(mtime).isoformat() - except Exception: - pass - return j(handler, { - "running": running, - "configured": configured, - "platforms": platforms, - "last_active": last_active, - "session_count": len(identity_map), - "health": { - "state": health_state, - "reason": health_reason, - "gateway_state": health_gateway_state, - }, - }) + return j(handler, _gateway_status_payload()) # ── MCP Servers (GET) ── if parsed.path == "/api/mcp/servers": @@ -9477,6 +9587,9 @@ def handle_post(handler, parsed) -> bool: if parsed.path == "/api/memory/write": return _handle_memory_write(handler, body) + if parsed.path in {"/api/gateway/start", "/api/gateway/stop", "/api/gateway/restart"}: + return _handle_gateway_lifecycle(handler, parsed.path.rsplit("/", 1)[-1], body) + # ── Profile API (POST) ── if parsed.path == "/api/profile/switch": name = body.get("name", "").strip() diff --git a/static/i18n.js b/static/i18n.js index 4b1301d6..47b4bf0f 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -186,6 +186,19 @@ const LOCALES = { provider_mismatch_warning: (m,p)=>`"${m}" may not work with your configured provider (${p}). Send anyway, or run \`hermes model\` in your terminal to switch.`, provider_mismatch_label: 'Provider mismatch', gateway_auth_label: 'Gateway authentication failed', + gateway_start: 'Start', + gateway_stop: 'Stop', + gateway_restart: 'Restart', + gateway_start_success: 'Gateway start completed.', + gateway_stop_success: 'Gateway stop completed.', + gateway_restart_success: 'Gateway restart completed.', + gateway_start_failed: 'Gateway start failed', + gateway_stop_failed: 'Gateway stop failed', + gateway_restart_failed: 'Gateway restart failed', + gateway_running: 'Running', + gateway_session: 'session', + gateway_sessions: 'sessions', + gateway_status_load_failed: 'Failed to load gateway status', model_not_found_label: 'Model not found', model_custom_label: 'Custom model ID', model_custom_placeholder: 'e.g. openai/gpt-5.4', @@ -1684,6 +1697,19 @@ const LOCALES = { provider_mismatch_warning: (m,p)=>`"${m}" potrebbe non funzionare con il provider configurato (${p}). Invia comunque, o esegui \`hermes model\` nel terminale per cambiare.`, provider_mismatch_label: 'Provider non corrispondente', gateway_auth_label: 'Autenticazione Gateway non riuscita', + gateway_start: 'Start', + gateway_stop: 'Stop', + gateway_restart: 'Restart', + gateway_start_success: 'Gateway start completed.', + gateway_stop_success: 'Gateway stop completed.', + gateway_restart_success: 'Gateway restart completed.', + gateway_start_failed: 'Gateway start failed', + gateway_stop_failed: 'Gateway stop failed', + gateway_restart_failed: 'Gateway restart failed', + gateway_running: 'Running', + gateway_session: 'session', + gateway_sessions: 'sessions', + gateway_status_load_failed: 'Failed to load gateway status', model_not_found_label: 'Modello non trovato', model_custom_label: 'ID modello personalizzato', model_custom_placeholder: 'es. openai/gpt-5.4', @@ -3173,6 +3199,19 @@ const LOCALES = { provider_mismatch_warning: (m,p)=>`"${m}" は設定されたプロバイダ (${p}) で動作しない可能性があります。このまま送信するか、ターミナルで \`hermes model\` を実行して切り替えてください。`, provider_mismatch_label: 'プロバイダ不一致', gateway_auth_label: 'ゲートウェイ認証に失敗しました', + gateway_start: 'Start', + gateway_stop: 'Stop', + gateway_restart: 'Restart', + gateway_start_success: 'Gateway start completed.', + gateway_stop_success: 'Gateway stop completed.', + gateway_restart_success: 'Gateway restart completed.', + gateway_start_failed: 'Gateway start failed', + gateway_stop_failed: 'Gateway stop failed', + gateway_restart_failed: 'Gateway restart failed', + gateway_running: 'Running', + gateway_session: 'session', + gateway_sessions: 'sessions', + gateway_status_load_failed: 'Failed to load gateway status', model_not_found_label: 'モデルが見つかりません', model_custom_label: 'カスタムモデルID', model_custom_placeholder: '例: openai/gpt-5.4', @@ -4639,6 +4678,19 @@ const LOCALES = { `"${m}" может не работать с вашим настроенным провайдером (${p}). Всё равно отправить или запустите \`hermes model\` в терминале, чтобы переключиться.`, provider_mismatch_label: 'Несовпадение провайдера', gateway_auth_label: 'Сбой аутентификации Gateway', + gateway_start: 'Start', + gateway_stop: 'Stop', + gateway_restart: 'Restart', + gateway_start_success: 'Gateway start completed.', + gateway_stop_success: 'Gateway stop completed.', + gateway_restart_success: 'Gateway restart completed.', + gateway_start_failed: 'Gateway start failed', + gateway_stop_failed: 'Gateway stop failed', + gateway_restart_failed: 'Gateway restart failed', + gateway_running: 'Running', + gateway_session: 'session', + gateway_sessions: 'sessions', + gateway_status_load_failed: 'Failed to load gateway status', model_not_found_label: 'Модель не найдена', model_custom_label: 'Пользовательский ID модели', model_custom_placeholder: 'например, openai/gpt-5.4', @@ -6072,6 +6124,19 @@ const LOCALES = { provider_mismatch_warning: (m,p)=>`"${m}" puede no funcionar con tu proveedor configurado (${p}). Envía de todas formas, o ejecuta \`hermes model\` en la terminal para cambiar.`, provider_mismatch_label: 'Proveedor incompatible', gateway_auth_label: 'Error de autenticación de Gateway', + gateway_start: 'Start', + gateway_stop: 'Stop', + gateway_restart: 'Restart', + gateway_start_success: 'Gateway start completed.', + gateway_stop_success: 'Gateway stop completed.', + gateway_restart_success: 'Gateway restart completed.', + gateway_start_failed: 'Gateway start failed', + gateway_stop_failed: 'Gateway stop failed', + gateway_restart_failed: 'Gateway restart failed', + gateway_running: 'Running', + gateway_session: 'session', + gateway_sessions: 'sessions', + gateway_status_load_failed: 'Failed to load gateway status', model_not_found_label: 'Modelo no encontrado', model_custom_label: 'ID de modelo personalizado', model_custom_placeholder: 'p. ej. openai/gpt-5.4', @@ -7497,6 +7562,19 @@ const LOCALES = { provider_mismatch_warning: (m,p)=>`"${m}" funktioniert möglicherweise nicht mit Ihrem konfigurierten Provider (${p}). Trotzdem senden, oder \`hermes model\` im Terminal ausführen.`, provider_mismatch_label: 'Provider-Konflikt', gateway_auth_label: 'Gateway-Authentifizierung fehlgeschlagen', + gateway_start: 'Start', + gateway_stop: 'Stop', + gateway_restart: 'Restart', + gateway_start_success: 'Gateway start completed.', + gateway_stop_success: 'Gateway stop completed.', + gateway_restart_success: 'Gateway restart completed.', + gateway_start_failed: 'Gateway start failed', + gateway_stop_failed: 'Gateway stop failed', + gateway_restart_failed: 'Gateway restart failed', + gateway_running: 'Running', + gateway_session: 'session', + gateway_sessions: 'sessions', + gateway_status_load_failed: 'Failed to load gateway status', model_not_found_label: 'Modell nicht gefunden', // commands.js cmd_help: 'Verfügbare Befehle auflisten', @@ -8926,6 +9004,19 @@ const LOCALES = { provider_mismatch_warning: (m,p)=>`\"${m}\" 可能无法在当前配置的提供商 (${p}) 下工作。直接发送,或在终端运行 \`hermes model\` 切换。`, provider_mismatch_label: '提供商不匹配', gateway_auth_label: 'Gateway 身份验证失败', + gateway_start: 'Start', + gateway_stop: 'Stop', + gateway_restart: 'Restart', + gateway_start_success: 'Gateway start completed.', + gateway_stop_success: 'Gateway stop completed.', + gateway_restart_success: 'Gateway restart completed.', + gateway_start_failed: 'Gateway start failed', + gateway_stop_failed: 'Gateway stop failed', + gateway_restart_failed: 'Gateway restart failed', + gateway_running: 'Running', + gateway_session: 'session', + gateway_sessions: 'sessions', + gateway_status_load_failed: 'Failed to load gateway status', model_not_found_label: '未找到模型', model_custom_label: '自定义模型 ID', model_custom_placeholder: '例如 openai/gpt-5.4', @@ -10375,6 +10466,19 @@ const LOCALES = { provider_mismatch_warning: (m,p)=>`「${m}」可能無法搭配已設定的供應商(${p})使用。仍要送出,或在終端機執行 \`hermes model\` 來切換。`, provider_mismatch_label: '供應商不相符', gateway_auth_label: 'Gateway 驗證失敗', + gateway_start: 'Start', + gateway_stop: 'Stop', + gateway_restart: 'Restart', + gateway_start_success: 'Gateway start completed.', + gateway_stop_success: 'Gateway stop completed.', + gateway_restart_success: 'Gateway restart completed.', + gateway_start_failed: 'Gateway start failed', + gateway_stop_failed: 'Gateway stop failed', + gateway_restart_failed: 'Gateway restart failed', + gateway_running: 'Running', + gateway_session: 'session', + gateway_sessions: 'sessions', + gateway_status_load_failed: 'Failed to load gateway status', model_not_found_label: '未找到模型', model_custom_label: '自訂模型 ID', model_custom_placeholder: '例如 openai/gpt-5.4', @@ -11775,6 +11879,24 @@ const LOCALES = { provider_mismatch_warning: (m,p)=>`"${m}" pode não funcionar com seu provedor configurado (${p}). Enviar assim mesmo, ou execute \`hermes model\` no terminal para trocar.`, provider_mismatch_label: 'Provedor incompatível', gateway_auth_label: 'Falha na autenticação do Gateway', + gateway_start: 'Start', + gateway_stop: 'Stop', + gateway_restart: 'Restart', + gateway_start_success: 'Gateway start completed.', + gateway_stop_success: 'Gateway stop completed.', + gateway_restart_success: 'Gateway restart completed.', + gateway_start_failed: 'Gateway start failed', + gateway_stop_failed: 'Gateway stop failed', + gateway_restart_failed: 'Gateway restart failed', + gateway_not_configured: 'Gateway not configured', + gateway_not_running: 'Gateway not running', + gateway_running: 'Running', + gateway_metadata_stale: 'Gateway metadata stale', + gateway_endpoint_unreachable: 'Gateway endpoint not reachable', + gateway_last_active: 'Last active', + gateway_session: 'session', + gateway_sessions: 'sessions', + gateway_status_load_failed: 'Failed to load gateway status', model_not_found_label: 'Modelo não encontrado', composer_mobile_workspace: 'Workspace', composer_mobile_model: 'Modelo', @@ -13145,6 +13267,19 @@ const LOCALES = { provider_mismatch_warning: (m,p)=>`"${m}" may not work with your configured provider (${p}). Send anyway, or run \`hermes model\` in your terminal to switch.`, provider_mismatch_label: 'Provider mismatch', gateway_auth_label: 'Gateway 인증 실패', + gateway_start: 'Start', + gateway_stop: 'Stop', + gateway_restart: 'Restart', + gateway_start_success: 'Gateway start completed.', + gateway_stop_success: 'Gateway stop completed.', + gateway_restart_success: 'Gateway restart completed.', + gateway_start_failed: 'Gateway start failed', + gateway_stop_failed: 'Gateway stop failed', + gateway_restart_failed: 'Gateway restart failed', + gateway_running: 'Running', + gateway_session: 'session', + gateway_sessions: 'sessions', + gateway_status_load_failed: 'Failed to load gateway status', model_not_found_label: 'Model not found', model_custom_label: 'Custom model ID', model_custom_placeholder: 'e.g. openai/gpt-5.4', @@ -14639,6 +14774,24 @@ const LOCALES = { model_unavailable_title: 'Ce modèle ne figure plus dans votre liste de fournisseurs actuelle', provider_mismatch_label: 'Inadéquation des fournisseurs', gateway_auth_label: 'Échec de l’authentification Gateway', + gateway_start: 'Start', + gateway_stop: 'Stop', + gateway_restart: 'Restart', + gateway_start_success: 'Gateway start completed.', + gateway_stop_success: 'Gateway stop completed.', + gateway_restart_success: 'Gateway restart completed.', + gateway_start_failed: 'Gateway start failed', + gateway_stop_failed: 'Gateway stop failed', + gateway_restart_failed: 'Gateway restart failed', + gateway_not_configured: 'Gateway not configured', + gateway_not_running: 'Gateway not running', + gateway_running: 'Running', + gateway_metadata_stale: 'Gateway metadata stale', + gateway_endpoint_unreachable: 'Gateway endpoint not reachable', + gateway_last_active: 'Last active', + gateway_session: 'session', + gateway_sessions: 'sessions', + gateway_status_load_failed: 'Failed to load gateway status', model_not_found_label: 'Modèle introuvable', model_custom_label: 'ID de modèle personnalisé', model_custom_placeholder: 'par ex. openai/gpt-5.4', @@ -16062,6 +16215,19 @@ const LOCALES = { provider_mismatch_warning: (m,p) => `"${m}" yapılandırılmış sağlayıcınızla (${p}) çalışmayabilir. Yine de gönderin veya geçiş yapmak için terminalinizde \`hermes model\` komutunu çalıştırın.`, provider_mismatch_label: 'Sağlayıcı uyumsuzluğu', gateway_auth_label: 'Gateway kimlik doğrulaması başarısız', + gateway_start: 'Start', + gateway_stop: 'Stop', + gateway_restart: 'Restart', + gateway_start_success: 'Gateway start completed.', + gateway_stop_success: 'Gateway stop completed.', + gateway_restart_success: 'Gateway restart completed.', + gateway_start_failed: 'Gateway start failed', + gateway_stop_failed: 'Gateway stop failed', + gateway_restart_failed: 'Gateway restart failed', + gateway_running: 'Running', + gateway_session: 'session', + gateway_sessions: 'sessions', + gateway_status_load_failed: 'Failed to load gateway status', model_not_found_label: 'Model bulunamadı', model_custom_label: 'Özel model kimliği', model_custom_placeholder: 'örneğin openai/gpt-5.4', @@ -17553,6 +17719,19 @@ const LOCALES = { provider_mismatch_warning: (m,p)=>`"${m}" może nie działać z Twoim skonfigurowanym dostawcą (${p}). Wyślij mimo to lub uruchom \`hermes model\` w terminalu, aby przełączyć.`, provider_mismatch_label: 'Niezgodność dostawcy', gateway_auth_label: 'Uwierzytelnianie bramy nie powiodło się', + gateway_start: 'Uruchom', + gateway_stop: 'Zatrzymaj', + gateway_restart: 'Uruchom ponownie', + gateway_start_success: 'Uruchamianie bramy zakończone.', + gateway_stop_success: 'Zatrzymywanie bramy zakończone.', + gateway_restart_success: 'Ponowne uruchamianie bramy zakończone.', + gateway_start_failed: 'Uruchamianie bramy nie powiodło się', + gateway_stop_failed: 'Zatrzymywanie bramy nie powiodło się', + gateway_restart_failed: 'Ponowne uruchamianie bramy nie powiodło się', + gateway_running: 'Działa', + gateway_session: 'sesja', + gateway_sessions: 'sesje', + gateway_status_load_failed: 'Nie udało się załadować statusu bramy', model_not_found_label: 'Model nie został znaleziony', model_custom_label: 'Niestandardowy ID modelu', model_custom_placeholder: 'np. openai/gpt-5.4', diff --git a/static/panels.js b/static/panels.js index b62030d7..3db726f3 100644 --- a/static/panels.js +++ b/static/panels.js @@ -9175,37 +9175,64 @@ function loadMcpTools(){ filterMcpTools(); }).catch(()=>{list.innerHTML=`