From 6d2051f813699c17d1fa683ffba38f872ffdcec4 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Wed, 20 May 2026 15:48:18 +0700 Subject: [PATCH] fix(web): improve error handling and API health check logic - Refactored the API health check to handle errors more gracefully, providing clearer feedback on loading issues. - Updated the NGINX configuration to use a dynamic upstream variable for better resilience against IP changes in Docker. - Enhanced user notifications for API availability and data loading errors, including instructions for local demo setup. --- deploy/docker/evobgp-web/nginx.conf | 9 ++- web/src/routes/+page.svelte | 87 +++++++++++++++++++++-------- 2 files changed, 72 insertions(+), 24 deletions(-) diff --git a/deploy/docker/evobgp-web/nginx.conf b/deploy/docker/evobgp-web/nginx.conf index 989a7fd..1245573 100644 --- a/deploy/docker/evobgp-web/nginx.conf +++ b/deploy/docker/evobgp-web/nginx.conf @@ -5,8 +5,13 @@ server { gzip on; gzip_types text/css application/javascript application/json image/svg+xml; + # Docker embedded DNS: без resolver nginx кэширует IP upstream при старте — + # после recreate evobgp-all остаётся 502 (connection refused на старый IP). + resolver 127.0.0.11 valid=10s ipv6=off; + set $evobgp_upstream evobgp-api; + location /v1/ { - proxy_pass http://evobgp-api:8080/v1/; + proxy_pass http://$evobgp_upstream:8080/v1/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -15,7 +20,7 @@ server { } location = /metrics { - proxy_pass http://evobgp-api:8080/metrics; + proxy_pass http://$evobgp_upstream:8080/metrics; proxy_http_version 1.1; proxy_set_header Host $host; } diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index 1b2a53b..7d1a39f 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -159,12 +159,29 @@ } ]); + function toErrorMessage(e: unknown): string { + return e instanceof Error ? e.message : String(e); + } + async function load() { if (!initialLoading) refreshing = true; loadError = null; try { - const [h, m, p, s, r, j] = await Promise.all([ - apiFetch('/v1/health'), + try { + const h = await apiFetch('/v1/health'); + healthy = h.ok; + if (!h.ok) { + loadError = `GET /v1/health: HTTP ${h.status}`; + return; + } + } catch (e) { + healthy = false; + loadError = toErrorMessage(e); + notifyApiError(e); + return; + } + + const [m, p, s, r, j] = await Promise.allSettled([ apiJSON('/v1/modules?limit=200'), apiJSON('/v1/peers?limit=200'), apiJSON('/v1/speakers?limit=200'), @@ -172,24 +189,38 @@ apiJSON('/v1/jobs?limit=20') ]); - healthy = h.ok; - moduleItems = m.items ?? []; - modulesHasMore = m.has_more; - peerItems = p.items ?? []; - peersHasMore = p.has_more; - speakerItems = s.items ?? []; - speakersHasMore = s.has_more; - revisionItems = r.items ?? []; - revisionsHasMore = r.has_more; - jobItems = j.items ?? []; - recentJobs = jobItems.slice(0, 10); - recentRevisions = revisionItems.slice(0, 10); - runningJobs = jobItems.filter((i) => i.status === 'running' || i.status === 'queued').length; - lastUpdated = new Date(); - } catch (e) { - healthy = false; - loadError = e instanceof Error ? e.message : String(e); - notifyApiError(e); + const firstReject = [m, p, s, r, j].find((x) => x.status === 'rejected'); + if (firstReject?.status === 'rejected') { + loadError = toErrorMessage(firstReject.reason); + notifyApiError(firstReject.reason); + } + + if (m.status === 'fulfilled') { + moduleItems = m.value.items ?? []; + modulesHasMore = m.value.has_more; + } + if (p.status === 'fulfilled') { + peerItems = p.value.items ?? []; + peersHasMore = p.value.has_more; + } + if (s.status === 'fulfilled') { + speakerItems = s.value.items ?? []; + speakersHasMore = s.value.has_more; + } + if (r.status === 'fulfilled') { + revisionItems = r.value.items ?? []; + revisionsHasMore = r.value.has_more; + } + if (j.status === 'fulfilled') { + jobItems = j.value.items ?? []; + recentJobs = jobItems.slice(0, 10); + recentRevisions = revisionItems.slice(0, 10); + runningJobs = jobItems.filter( + (i) => i.status === 'running' || i.status === 'queued' + ).length; + } + + if (!loadError) lastUpdated = new Date(); } finally { initialLoading = false; refreshing = false; @@ -235,18 +266,30 @@ Проверка API… Запрос к /v1/health - {:else if healthy} + {:else if healthy && !loadError} API работает Сервер отвечает на запросы health-check. + {:else if healthy && loadError} + + + API доступен, данные не загружены + + {loadError}. Для локального демо укажите Bearer-токен + dev в + + (нужен EVOBGP_DEV_INSECURE=1 на API). + + {:else} API недоступен - Не удалось получить ответ от сервера. Проверьте подключение и статус API. + {loadError ?? + 'Не удалось получить ответ от сервера. Проверьте, что API запущен (порт 8080), в dev — `npm run dev` с прокси Vite, в Docker — контейнер evobgp-api / evobgp-all и nginx в evobgp-web.'} {/if}