From 3e660c39bbbb147b58fe735841a3a86048840c1f Mon Sep 17 00:00:00 2001 From: ochenstarik-ui <267932263+ochenstarik-ui@users.noreply.github.com> Date: Mon, 31 Aug 2026 23:39:53 +0700 Subject: [PATCH] fix(a55): account connection fixes - antigravity windows/linux, ollama server discovery, version display, full settings --- src/antigravity_provider/agy_subprocess.py | 56 ++++- .../router/action_handler.py | 12 +- .../router/adapters/ollama_adapter.py | 23 +- .../router/connection_preflight.py | 8 + .../router/model_discovery_service.py | 51 +++- .../router/profile_manager.py | 47 +++- .../router/state_store.py | 5 + src/antigravity_provider/router/web/server.py | 21 +- .../router/web/static/app.js | 230 +++++++++++++++--- src/antigravity_provider/version.py | 2 +- tests/test_a42_provider_connect.py | 2 +- tests/test_a55_account_connection.py | 188 ++++++++++++++ tests/test_api_providers_a32.py | 2 +- uv.lock | 2 +- 14 files changed, 566 insertions(+), 83 deletions(-) create mode 100644 tests/test_a55_account_connection.py diff --git a/src/antigravity_provider/agy_subprocess.py b/src/antigravity_provider/agy_subprocess.py index afda537..bf59474 100644 --- a/src/antigravity_provider/agy_subprocess.py +++ b/src/antigravity_provider/agy_subprocess.py @@ -124,17 +124,57 @@ def discover_models(profile_id: str | None = None) -> dict[str, str]: # отвечала «Please sign in to view available models» — при шести рабочих # OAuth-профилях. Список моделей поэтому был пуст всегда. target_profile_id = profile_id - if not target_profile_id: + if target_profile_id: try: from antigravity_provider.router.profile_manager import ProfileAuthManager + st = ProfileAuthManager.get_profile_status("antigravity", target_profile_id) + if not st.get("authenticated") and not ProfileAuthManager.load_profile_auth("antigravity", target_profile_id): + target_profile_id = None + except Exception: + pass + + if not target_profile_id: + try: + from antigravity_provider.paths import get_hermes_home + from antigravity_provider.router.profile_manager import ProfileAuthManager + from antigravity_provider.router.router_config import load_router_config + + candidate_pids: list[str] = [] + try: + cfg = load_router_config() + for pid, pcfg in cfg.profiles.items(): + if pcfg.provider.lower() in ("antigravity", "google-antigravity") and pid not in candidate_pids: + candidate_pids.append(pid) + except Exception: + pass + main_p = ProfileAuthManager.get_main_profile("antigravity") - if main_p and ProfileAuthManager.load_profile_auth("antigravity", main_p): - target_profile_id = main_p - else: - for candidate in ["ag-orch-primary", "ag-w1", "ag-w2", "ag-w3", "ag-w4", "ag-w5"]: - if ProfileAuthManager.load_profile_auth("antigravity", candidate): - target_profile_id = candidate - break + if main_p and main_p not in candidate_pids: + candidate_pids.append(main_p) + + standard_slots = ( + ["ag-orch-primary", "ag-orch-fallback"] + + [f"ag-{i}" for i in range(1, 21)] + + [f"ag-w{i}" for i in range(1, 11)] + ) + for s in standard_slots: + if s not in candidate_pids: + candidate_pids.append(s) + + try: + agy_dir = get_hermes_home() / "agy_profiles" + if agy_dir.is_dir(): + for sub in sorted(agy_dir.iterdir()): + if sub.is_dir() and sub.name not in candidate_pids: + candidate_pids.append(sub.name) + except Exception: + pass + + for candidate in candidate_pids: + st = ProfileAuthManager.get_profile_status("antigravity", candidate) + if st.get("authenticated") or ProfileAuthManager.load_profile_auth("antigravity", candidate): + target_profile_id = candidate + break except Exception: target_profile_id = None diff --git a/src/antigravity_provider/router/action_handler.py b/src/antigravity_provider/router/action_handler.py index 11620b9..7198a51 100644 --- a/src/antigravity_provider/router/action_handler.py +++ b/src/antigravity_provider/router/action_handler.py @@ -51,12 +51,12 @@ def do_test_profile(provider: str, profile_id: str, timeout: float = 10.0, disco return {'success': False, 'error': f"Профиль '{profile_id}' не найден"} status = ProfileAuthManager.get_profile_status(pcfg.provider, profile_id) - if not status.get('authenticated'): - return {'success': False, 'error': 'Аккаунт не добавлен. Сначала выполните подключение.'} - if status.get('is_expired') or status.get('expired') or status.get('status') == 'EXPIRED': return {'success': False, 'error': 'Авторизация истекла, требуется повторный вход.'} + if not status.get('authenticated'): + return {'success': False, 'error': 'Аккаунт не добавлен. Сначала выполните подключение.'} + candidates = discovered_models if discovered_models is not None else pcfg.preferred_models if not candidates: return {'success': False, 'error': 'Сервер отвечает, но доступных моделей для тестового запроса нет' if discovered_models == [] else 'Каталог моделей не получен; сначала запросите список моделей'} @@ -753,6 +753,8 @@ class ActionExecutor: return {'ok': False, 'message': f'Провайдер {prov_norm} не поддерживается для прямого добавления учетных данных'} existing_status = ProfileAuthManager.get_profile_status(prov_norm, slot) if slot else {} if not token and prov_norm not in ('local', 'vllm', 'ollama') and not existing_status.get('authenticated'): + if prov_norm in ('antigravity', 'google-antigravity', 'claude', 'anthropic'): + return {'ok': False, 'message': 'Авторизация через браузер не завершена. Пожалуйста, откройте ссылку входа или вставьте адрес возврата.'} return {'ok': False, 'message': 'Не указан API-ключ или не завершена авторизация'} validation = None if token or prov_norm in ('local', 'vllm', 'ollama'): @@ -790,7 +792,7 @@ class ActionExecutor: return {'ok': False, 'message': 'Не указан API-ключ для NVIDIA NIM'} elif prov_norm in ('claude', 'anthropic'): if not token: - return {'ok': False, 'message': 'Не указан API-ключ для Claude'} + return {'ok': False, 'message': 'Авторизация через браузер не завершена. Пожалуйста, откройте ссылку входа или вставьте адрес возврата.'} elif prov_norm in ('opencode-go', 'opencode'): if not token: return {'ok': False, 'message': 'Не указан API-ключ для OpenCode Go'} @@ -802,7 +804,7 @@ class ActionExecutor: return {'ok': False, 'message': f'Не указан API-ключ для {prov_norm}'} elif prov_norm in ('antigravity', 'google-antigravity'): if not token and not is_authenticated: - return {'ok': False, 'message': 'Не выполнена авторизация для Antigravity'} + return {'ok': False, 'message': 'Авторизация через браузер не завершена. Пожалуйста, откройте ссылку входа или вставьте адрес возврата.'} else: return {'ok': False, 'message': f'Провайдер {prov_norm} не поддерживается для прямого добавления учетных данных'} diff --git a/src/antigravity_provider/router/adapters/ollama_adapter.py b/src/antigravity_provider/router/adapters/ollama_adapter.py index 330e216..dd567e4 100644 --- a/src/antigravity_provider/router/adapters/ollama_adapter.py +++ b/src/antigravity_provider/router/adapters/ollama_adapter.py @@ -14,7 +14,7 @@ from .local_adapter import LocalLLMAdapter logger = logging.getLogger("hermes.router.adapter.ollama") -DEFAULT_OLLAMA_BASE_URL = "http://127.0.0.1:11434/v1" +DEFAULT_OLLAMA_BASE_URL = "http://127.0.0.1:11434" DEFAULT_OLLAMA_MODELS = ["llama3:latest"] @@ -54,21 +54,24 @@ class OllamaAdapter(LocalLLMAdapter): def _get_native_host(self, base_url: str) -> str: """Strip trailing /v1 from base_url to get native Ollama host.""" - if base_url.endswith("/v1"): - return base_url[:-3] - return base_url + b = base_url.rstrip("/") + if b.endswith("/v1"): + return b[:-3].rstrip("/") + return b def _get_chat_url(self, base_url: str) -> str: """Get standard chat completions endpoint URL.""" - if base_url.endswith("/v1"): - return f"{base_url}/chat/completions" - return f"{base_url}/v1/chat/completions" + b = base_url.rstrip("/") + if b.endswith("/v1"): + return f"{b}/chat/completions" + return f"{b}/v1/chat/completions" def _get_models_url(self, base_url: str) -> str: """Get OpenAI-compatible models endpoint URL.""" - if base_url.endswith("/v1"): - return f"{base_url}/models" - return f"{base_url}/v1/models" + b = base_url.rstrip("/") + if b.endswith("/v1"): + return f"{b}/models" + return f"{b}/v1/models" def invoke(self, profile: RouterProfileConfig, request: Dict[str, Any]) -> Dict[str, Any]: base_url = self._resolve_base_url(profile) diff --git a/src/antigravity_provider/router/connection_preflight.py b/src/antigravity_provider/router/connection_preflight.py index 612279e..e1baee9 100644 --- a/src/antigravity_provider/router/connection_preflight.py +++ b/src/antigravity_provider/router/connection_preflight.py @@ -95,6 +95,14 @@ def validate_connection(provider, token="", base_url="", preferred_model=""): message = f"Подключено и проверено. Получено моделей: {len(models)}" if models else "Сервер отвечает; моделей пока нет" return {"ok": True, "message": message, "data": {"models": models, "base_url": base_url}} except Exception as exc: + exc_str = str(exc) + exc_lower = exc_str.lower() + if provider == "ollama" and any(k in exc_lower for k in ("connection refused", "winerror 10061", "errno 111", "111", "refused", "failed to connect", "target machine actively refused")): + return { + "ok": False, + "message": f"Не удалось подключиться к {base_url} (Connection refused). Если Ollama работает на сервере или другой машине, укажите её сетевой адрес (например, http://192.168.1.81:11434).", + "data": {"models": []}, + } if isinstance(exc, urllib.error.HTTPError): reason = exc.reason or 'провайдер отклонил запрос' try: diff --git a/src/antigravity_provider/router/model_discovery_service.py b/src/antigravity_provider/router/model_discovery_service.py index 37954f4..ad07acd 100644 --- a/src/antigravity_provider/router/model_discovery_service.py +++ b/src/antigravity_provider/router/model_discovery_service.py @@ -369,9 +369,56 @@ class ModelDiscoveryService: if prov in ("antigravity", "google-antigravity"): from antigravity_provider.agy_subprocess import discover_models - main_p = getattr(self._probe_context, "profile_id", None) or ProfileAuthManager.get_main_profile("antigravity") or "ag-orch-fallback" + from antigravity_provider.paths import get_hermes_home + from antigravity_provider.router.router_config import load_router_config + + candidate_pids: List[str] = [] + req_p = getattr(self._probe_context, "profile_id", None) + if req_p: + candidate_pids.append(req_p) + try: - res = discover_models(profile_id=main_p) + cfg = load_router_config() + for pid, pcfg in cfg.profiles.items(): + if pcfg.provider.lower() in ("antigravity", "google-antigravity") and pid not in candidate_pids: + candidate_pids.append(pid) + except Exception: + pass + + main_p = ProfileAuthManager.get_main_profile("antigravity") + if main_p and main_p not in candidate_pids: + candidate_pids.append(main_p) + + standard_slots = ( + ["ag-orch-primary", "ag-orch-fallback"] + + [f"ag-{i}" for i in range(1, 21)] + + [f"ag-w{i}" for i in range(1, 11)] + ) + for s in standard_slots: + if s not in candidate_pids: + candidate_pids.append(s) + + try: + agy_dir = get_hermes_home() / "agy_profiles" + if agy_dir.is_dir(): + for sub in sorted(agy_dir.iterdir()): + if sub.is_dir() and sub.name not in candidate_pids: + candidate_pids.append(sub.name) + except Exception: + pass + + target_pid = None + for cand in candidate_pids: + st = ProfileAuthManager.get_profile_status("antigravity", cand) + if st.get("authenticated") or ProfileAuthManager.load_profile_auth("antigravity", cand): + target_pid = cand + break + + if not target_pid: + target_pid = candidate_pids[0] if candidate_pids else "ag-orch-fallback" + + try: + res = discover_models(profile_id=target_pid) if res: return sorted(list(set(res.values()))), None return None, "Модели Google Antigravity не обнаружены" diff --git a/src/antigravity_provider/router/profile_manager.py b/src/antigravity_provider/router/profile_manager.py index 99a1309..174a1c8 100644 --- a/src/antigravity_provider/router/profile_manager.py +++ b/src/antigravity_provider/router/profile_manager.py @@ -423,6 +423,17 @@ class ProfileAuthManager: except Exception as e: logger.warning("Error reading %s: %s", auth_file, e) + if not auth_file.is_file() and provider in ("antigravity", "google-antigravity"): + pdir = get_profile_dir(profile_id, provider) + gemini_creds = pdir / ".gemini" / "oauth_creds.json" + if gemini_creds.is_file(): + try: + data = json.loads(gemini_creds.read_text(encoding="utf-8")) + if isinstance(data, dict): + return {"provider": provider, "profile_id": profile_id, "token": data} + except Exception as e: + logger.warning("Error reading %s: %s", gemini_creds, e) + # Fallbacks for specific providers if provider == "openai-codex": env_var = f"CODEX_TOKEN_{profile_id.upper().replace('-', '_')}" @@ -655,11 +666,24 @@ class ProfileAuthManager: } if provider in ("antigravity", "google-antigravity"): - tokens = auth_data.get("token") or auth_data.get("tokens", {}) - acc_token = tokens.get("access_token") if isinstance(tokens, dict) else (auth_data.get("access_token") or "") - id_token = tokens.get("id_token") if isinstance(tokens, dict) else (auth_data.get("id_token") or "") - refresh_tok = tokens.get("refresh_token") if isinstance(tokens, dict) else (auth_data.get("refresh_token") or "") - email = auth_data.get("email") + tokens = auth_data.get("token") or auth_data.get("tokens") + if not isinstance(tokens, dict): + tokens = {} + acc_token = tokens.get("access_token") or auth_data.get("access_token") or "" + id_token = tokens.get("id_token") or auth_data.get("id_token") or "" + refresh_tok = tokens.get("refresh_token") or auth_data.get("refresh_token") or "" + key = auth_data.get("api_key", "") + email = auth_data.get("email") or auth_data.get("user_email") + is_auth = bool(acc_token or refresh_tok or key or (email and auth_data.get("auth_method") == "oauth")) + if not is_auth: + return { + "authenticated": False, + "provider": provider, + "profile_id": profile_id, + "status": "NOT_CONFIGURED", + "error": None, + } + acc_id = None if id_token: email_from_jwt, acc_id = cls.extract_jwt_identity(id_token) @@ -668,11 +692,14 @@ class ProfileAuthManager: email_from_jwt, acc_id = cls.extract_jwt_identity(acc_token) email = email or email_from_jwt - expiry = tokens.get("expiry_date") if isinstance(tokens, dict) else auth_data.get("expiry_date") - if not expiry and isinstance(tokens, dict): - expiry = tokens.get("expires_at") + expiry = ( + tokens.get("expiry_date") + or auth_data.get("expiry_date") + or tokens.get("expires_at") + or auth_data.get("expires_at") + ) if not expiry: - expiry_str = tokens.get("expiry") if isinstance(tokens, dict) else auth_data.get("expiry") + expiry_str = tokens.get("expiry") or auth_data.get("expiry") if expiry_str: try: dt = datetime.fromisoformat(str(expiry_str).replace("Z", "+00:00")) @@ -688,7 +715,7 @@ class ProfileAuthManager: is_expired = not bool(refresh_tok) return { - "authenticated": True, + "authenticated": not is_expired, "provider": provider, "profile_id": profile_id, "email_masked": mask_email(email) if email else None, diff --git a/src/antigravity_provider/router/state_store.py b/src/antigravity_provider/router/state_store.py index 3eb4f97..30033aa 100644 --- a/src/antigravity_provider/router/state_store.py +++ b/src/antigravity_provider/router/state_store.py @@ -11,6 +11,8 @@ import time from dataclasses import dataclass, field, replace from typing import Any, Dict, List, Optional, Tuple +from antigravity_provider.version import __version__ + from antigravity_provider.router.event_bus import ( EventBus, EVENT_ACCOUNT_UPDATED, @@ -213,7 +215,9 @@ class HubStateStore: hermes_cfg = {"exists": False, "model": None, "provider": None} default_role = "manager" + from antigravity_provider.version import __version__ metrics = { + "version": __version__, "generation": gen, "seq": request_seq, "duration_ms": round((time.time() - t0) * 1000, 2), @@ -311,6 +315,7 @@ class HubStateStore: routing={}, quotas={}, metrics={ + "version": __version__, "generation": 0, "seq": 0, "telemetry": telemetry_data, diff --git a/src/antigravity_provider/router/web/server.py b/src/antigravity_provider/router/web/server.py index 88f2e93..6cb6f94 100644 --- a/src/antigravity_provider/router/web/server.py +++ b/src/antigravity_provider/router/web/server.py @@ -234,6 +234,13 @@ def get_snapshot(authorized: bool = Depends(get_auth_token)): ), } + snap_dict["version"] = __version__ + snap_dict["commit"] = get_installed_commit() + snap_dict["system_paths"] = { + "hermes_home": str(paths.get_hermes_home()), + "config_dir": str(paths.get_config_dir()), + "log_file": str(paths.get_log_file()), + } return JSONResponse(content=jsonable_encoder(snap_dict)) @app.post("/api/action") @@ -387,15 +394,21 @@ def get_settings(authorized: bool = Depends(get_auth_token)): last_check = UpdateManager.get_last_check_result() server_host = raw.get("web_api_host", "127.0.0.1") is_external = (server_host != "127.0.0.1" and server_host != "localhost") + system_paths = { + "hermes_home": str(paths.get_hermes_home()), + "config_dir": str(paths.get_config_dir()), + "log_file": str(paths.get_log_file()), + } settings_out: Dict[str, Any] = { + "system_paths": system_paths, "web_api_host": server_host, "web_api_port": raw.get("web_api_port", 5800), "web_api_token_configured": has_token, "theme": raw.get("theme", "system"), - "quota_refresh_interval_sec": raw.get("quota_refresh_interval_sec", 300), - "hermes_home": str(paths.get_hermes_home()), - "config_dir": str(paths.get_config_dir()), - "log_file": str(paths.get_log_file()), + "quota_refresh_interval_sec": raw.get("quota_refresh_interval_sec", raw.get("account_check_interval_seconds", 300)), + "hermes_home": system_paths["hermes_home"], + "config_dir": system_paths["config_dir"], + "log_file": system_paths["log_file"], "installed_commit": get_installed_commit(), "version": __version__, "last_update_check": last_check.to_dict() if last_check else None, diff --git a/src/antigravity_provider/router/web/static/app.js b/src/antigravity_provider/router/web/static/app.js index 85ef806..440fb9e 100644 --- a/src/antigravity_provider/router/web/static/app.js +++ b/src/antigravity_provider/router/web/static/app.js @@ -71,6 +71,7 @@ document.addEventListener('DOMContentLoaded', () => { initNavigation(); initEventListeners(); initSettings(); + fetchSettings(); fetchSnapshot(); startPolling(); checkUpdates(true); @@ -114,6 +115,10 @@ function switchView(viewName) { fetchSkills(); } + if (viewName === 'settings') { + fetchSettings(); + } + if (currentSnapshot) { renderCurrentView(); } @@ -620,6 +625,12 @@ function updateGlobalHeader() { if (kpiReadyRoles) kpiReadyRoles.textContent = `${readyRoles}/${totalRoles}`; if (kpiRolesSub) kpiRolesSub.textContent = `${readyRoles} из ${totalRoles} ролей маршрутизации активны`; if (kpiProvidersCount) kpiProvidersCount.textContent = (currentSnapshot.providers || []).length; + + const curVer = (currentSnapshot && (currentSnapshot.version || (currentSnapshot.metrics || {}).version)) || (currentSettings && currentSettings.version) || ''; + const versionTag = document.getElementById('version-tag'); + if (versionTag) { + versionTag.textContent = curVer ? `Hermes Hub Web v${curVer}` : 'Hermes Hub Web — Н/Д: версия не передана сервером'; + } } // ── VIEW ROUTER ── @@ -1616,86 +1627,166 @@ function renderLogsList(events) { } // ── SETTINGS MANAGEMENT ── +async function fetchSettings() { + try { + const headers = authToken ? { 'X-Hub-Token': authToken } : {}; + const res = await fetch('/api/settings', { headers }); + if (res.ok) { + currentSettings = await res.json(); + if (activeView === 'settings') { + renderSettingsView(); + } + return currentSettings; + } + } catch (err) { + console.error('Failed to fetch settings:', err); + } + return null; +} + function renderSettingsView() { - if (!currentSnapshot) return; - const paths = {}; // HubSnapshot does not expose filesystem paths. - const s = currentSettings; + const s = currentSettings || {}; + const sysPaths = s.system_paths || (currentSnapshot && currentSnapshot.system_paths) || {}; const elHome = document.getElementById('path-hermes-home'); const elConfig = document.getElementById('path-config-dir'); const elLog = document.getElementById('path-log-file'); - if (elHome) elHome.textContent = paths.hermes_home || 'Н/Д: API не передаёт путь'; - if (elConfig) elConfig.textContent = paths.config_dir || 'Н/Д: API не передаёт путь'; - if (elLog) elLog.textContent = paths.log_file || 'Н/Д: API не передаёт путь'; + const homePath = sysPaths.hermes_home || s.hermes_home; + const configPath = sysPaths.config_dir || s.config_dir; + const logPath = sysPaths.log_file || s.log_file; - const quotaThresholdSel = document.getElementById('setting-quota-threshold-percent'); - const quotaActionSel = document.getElementById('setting-quota-threshold-action'); - const emailMaskingSel = document.getElementById('setting-email-masking-mode'); - const monitorIntervalInput = document.getElementById('setting-monitoring-interval'); + if (elHome) elHome.textContent = homePath || 'Н/Д: путь не передан сервером'; + if (elConfig) elConfig.textContent = configPath || 'Н/Д: путь не передан сервером'; + if (elLog) elLog.textContent = logPath || 'Н/Д: путь не передан сервером'; + + // Server Host & Port + const hostInput = document.getElementById('setting-server-host'); + const portInput = document.getElementById('setting-server-port'); + if (hostInput && s.web_api_host !== undefined) { + hostInput.value = s.web_api_host; + } + if (portInput && s.web_api_port !== undefined) { + portInput.value = s.web_api_port; + } + + // Token Badge + const tokenBadge = document.getElementById('setting-token-status-badge'); + if (tokenBadge) { + const isTokenSet = Boolean(s.web_api_token_configured || s.web_api_token); + if (isTokenSet) { + tokenBadge.textContent = '✓ Задан'; + tokenBadge.className = 'badge healthy'; + } else { + tokenBadge.textContent = 'Не задан'; + tokenBadge.className = 'badge'; + } + } + + // Account Check Interval const accountIntervalInput = document.getElementById('setting-account-check-interval'); - const defaultRoleSel = document.getElementById('setting-default-role'); + if (accountIntervalInput) { + const accVal = s.account_check_interval_seconds ?? s.account_interval; + if (accVal !== undefined && Number.isFinite(Number(accVal))) { + accountIntervalInput.value = accVal; + accountIntervalInput.disabled = false; + accountIntervalInput.placeholder = '300'; + } else { + accountIntervalInput.placeholder = 'Н/Д: не передан сервером'; + } + } + // Quota Interval + const quotaIntervalSel = document.getElementById('setting-quota-interval'); + if (quotaIntervalSel) { + const qVal = s.quota_refresh_interval_sec ?? s.quota_interval ?? s.account_check_interval_seconds; + if (qVal !== undefined) { + quotaIntervalSel.value = String(qVal); + } + } + + // Quota Threshold Percent + const quotaThresholdSel = document.getElementById('setting-quota-threshold-percent'); if (quotaThresholdSel && s.quota_threshold_percent !== undefined) { quotaThresholdSel.value = String(Math.round(s.quota_threshold_percent)); } + + // Quota Threshold Action + const quotaActionSel = document.getElementById('setting-quota-threshold-action'); if (quotaActionSel && s.quota_threshold_action) { quotaActionSel.value = s.quota_threshold_action; } + + // Email Masking Mode + const emailMaskingSel = document.getElementById('setting-email-masking-mode'); if (emailMaskingSel && s.email_masking_mode) { emailMaskingSel.value = s.email_masking_mode; } - if (accountIntervalInput && !accountIntervalInput.dataset.loaded) { - accountIntervalInput.dataset.loaded = 'loading'; - fetch('/api/settings', {headers: authToken ? {'X-Hub-Token': authToken} : {}}) - .then(response => { if (!response.ok) throw new Error('Настройки недоступны'); return response.json(); }) - .then(settings => { - if (!Number.isFinite(Number(settings.account_check_interval_seconds))) throw new Error('Период не передан сервером'); - accountIntervalInput.value = settings.account_check_interval_seconds; - accountIntervalInput.disabled = false; - accountIntervalInput.dataset.loaded = 'yes'; - }).catch(error => { accountIntervalInput.placeholder = 'Н/Д: ' + error.message; accountIntervalInput.dataset.loaded = ''; }); + + // Default Role + const defaultRoleSel = document.getElementById('setting-default-role'); + if (defaultRoleSel) { + const currentDef = s.default_role || (currentSnapshot && currentSnapshot.metrics && currentSnapshot.metrics.default_role) || 'manager'; + defaultRoleSel.value = currentDef; } + + // Theme + const themeSel = document.getElementById('setting-theme'); + if (themeSel && s.theme) { + themeSel.value = s.theme; + } + + // Monitor Interval + const monitorIntervalInput = document.getElementById('setting-monitoring-interval'); if (monitorIntervalInput && s.monitoring_interval_seconds !== undefined) { monitorIntervalInput.value = s.monitoring_interval_seconds; } + // Obsidian Vault Path const vaultPathInput = document.getElementById('setting-obsidian-vault-path'); if (vaultPathInput) { vaultPathInput.value = s.obsidian_vault_path || '/srv/projects/AI-Memory'; } - if (defaultRoleSel) { - const currentDef = s.default_role || currentSnapshot.metrics?.default_role || 'manager'; - defaultRoleSel.value = currentDef; - } } async function saveHubServerSettings() { + const hostInput = document.getElementById('setting-server-host'); + const portInput = document.getElementById('setting-server-port'); + const tokenInput = document.getElementById('setting-server-token-input'); const quotaThresholdSel = document.getElementById('setting-quota-threshold-percent'); const quotaActionSel = document.getElementById('setting-quota-threshold-action'); const emailMaskingSel = document.getElementById('setting-email-masking-mode'); + const quotaIntervalSel = document.getElementById('setting-quota-interval'); const monitorIntervalInput = document.getElementById('setting-monitoring-interval'); const vaultPathInput = document.getElementById('setting-obsidian-vault-path'); const accountIntervalInput = document.getElementById('setting-account-check-interval'); const defaultRoleSel = document.getElementById('setting-default-role'); + const themeSel = document.getElementById('setting-theme'); const newSettings = {}; - if (accountIntervalInput?.value) newSettings.account_check_interval_seconds = Math.max(60, Number(accountIntervalInput.value)); - if (quotaThresholdSel?.value) newSettings.quota_threshold_percent = Number(quotaThresholdSel.value); - if (quotaActionSel?.value) newSettings.quota_threshold_action = quotaActionSel.value; - if (emailMaskingSel?.value) newSettings.email_masking_mode = emailMaskingSel.value; - if (monitorIntervalInput?.value) newSettings.monitoring_interval_seconds = Number(monitorIntervalInput.value); - if (vaultPathInput?.value) newSettings.obsidian_vault_path = vaultPathInput.value.trim(); - if (defaultRoleSel?.value) newSettings.default_role = defaultRoleSel.value; + if (hostInput && hostInput.value.trim()) newSettings.web_api_host = hostInput.value.trim(); + if (portInput && portInput.value) newSettings.web_api_port = Number(portInput.value); + if (tokenInput && tokenInput.value.trim()) newSettings.web_api_token = tokenInput.value.trim(); + if (accountIntervalInput && accountIntervalInput.value) newSettings.account_check_interval_seconds = Math.max(60, Number(accountIntervalInput.value)); + if (quotaIntervalSel && quotaIntervalSel.value) newSettings.quota_refresh_interval_sec = Number(quotaIntervalSel.value); + if (quotaThresholdSel && quotaThresholdSel.value) newSettings.quota_threshold_percent = Number(quotaThresholdSel.value); + if (quotaActionSel && quotaActionSel.value) newSettings.quota_threshold_action = quotaActionSel.value; + if (emailMaskingSel && emailMaskingSel.value) newSettings.email_masking_mode = emailMaskingSel.value; + if (monitorIntervalInput && monitorIntervalInput.value) newSettings.monitoring_interval_seconds = Number(monitorIntervalInput.value); + if (vaultPathInput && vaultPathInput.value.trim()) newSettings.obsidian_vault_path = vaultPathInput.value.trim(); + if (defaultRoleSel && defaultRoleSel.value) newSettings.default_role = defaultRoleSel.value; + if (themeSel && themeSel.value) newSettings.theme = themeSel.value; + if (!Object.keys(newSettings).length) { showToast('Нет выбранных изменений', 'info'); return; } showToast('Сохранение настроек сервера...', 'info'); const res = await executeAction('save_settings', newSettings); - if (res.ok) { + if (res && res.ok) { showToast('Настройки сервера успешно сохранены', 'success'); + await fetchSettings(); fetchSnapshot(); } else { - showToast(res.message || 'Ошибка сохранения настроек сервера', 'error'); + showToast((res && res.message) || 'Ошибка сохранения настроек сервера', 'error'); } } @@ -2826,7 +2917,7 @@ function showWizardStep2(providerId) {