Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
293115e0e1 | ||
|
|
6d2051f813 |
@@ -5,8 +5,14 @@ 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 нельзя полагаться на замену URI — передаём $request_uri целиком.
|
||||
proxy_pass http://$evobgp_upstream:8080$request_uri;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -15,7 +21,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;
|
||||
}
|
||||
|
||||
+65
-22
@@ -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<ModulesResponse>('/v1/modules?limit=200'),
|
||||
apiJSON<PeersResponse>('/v1/peers?limit=200'),
|
||||
apiJSON<SpeakersResponse>('/v1/speakers?limit=200'),
|
||||
@@ -172,24 +189,38 @@
|
||||
apiJSON<JobsResponse>('/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 @@
|
||||
<AlertTitle>Проверка API…</AlertTitle>
|
||||
<AlertDescription>Запрос к <code class="text-xs">/v1/health</code></AlertDescription>
|
||||
</Alert>
|
||||
{:else if healthy}
|
||||
{:else if healthy && !loadError}
|
||||
<Alert class="border-success/30 bg-success/5">
|
||||
<CheckCircle class="text-success" />
|
||||
<AlertTitle>API работает</AlertTitle>
|
||||
<AlertDescription>Сервер отвечает на запросы health-check.</AlertDescription>
|
||||
</Alert>
|
||||
{:else if healthy && loadError}
|
||||
<Alert class="border-warning/30 bg-warning/5">
|
||||
<Info class="text-warning" />
|
||||
<AlertTitle>API доступен, данные не загружены</AlertTitle>
|
||||
<AlertDescription>
|
||||
{loadError}. Для локального демо укажите Bearer-токен
|
||||
<code class="text-xs">dev</code> в
|
||||
<Button variant="link" class="h-auto p-0" href={resolve('/settings')}>Настройках</Button>
|
||||
(нужен <code class="text-xs">EVOBGP_DEV_INSECURE=1</code> на API).
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
{:else}
|
||||
<Alert variant="destructive" class="border-destructive/30 bg-destructive/5">
|
||||
<XCircle class="text-destructive" />
|
||||
<AlertTitle>API недоступен</AlertTitle>
|
||||
<AlertDescription>
|
||||
Не удалось получить ответ от сервера. Проверьте подключение и статус API.
|
||||
{loadError ??
|
||||
'Не удалось получить ответ от сервера. Проверьте, что API запущен (порт 8080), в dev — `npm run dev` с прокси Vite, в Docker — контейнер evobgp-api / evobgp-all и nginx в evobgp-web.'}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user