Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d2051f813 |
@@ -5,8 +5,13 @@ server {
|
|||||||
gzip on;
|
gzip on;
|
||||||
gzip_types text/css application/javascript application/json image/svg+xml;
|
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/ {
|
location /v1/ {
|
||||||
proxy_pass http://evobgp-api:8080/v1/;
|
proxy_pass http://$evobgp_upstream:8080/v1/;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
@@ -15,7 +20,7 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location = /metrics {
|
location = /metrics {
|
||||||
proxy_pass http://evobgp-api:8080/metrics;
|
proxy_pass http://$evobgp_upstream:8080/metrics;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
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() {
|
async function load() {
|
||||||
if (!initialLoading) refreshing = true;
|
if (!initialLoading) refreshing = true;
|
||||||
loadError = null;
|
loadError = null;
|
||||||
try {
|
try {
|
||||||
const [h, m, p, s, r, j] = await Promise.all([
|
try {
|
||||||
apiFetch('/v1/health'),
|
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<ModulesResponse>('/v1/modules?limit=200'),
|
||||||
apiJSON<PeersResponse>('/v1/peers?limit=200'),
|
apiJSON<PeersResponse>('/v1/peers?limit=200'),
|
||||||
apiJSON<SpeakersResponse>('/v1/speakers?limit=200'),
|
apiJSON<SpeakersResponse>('/v1/speakers?limit=200'),
|
||||||
@@ -172,24 +189,38 @@
|
|||||||
apiJSON<JobsResponse>('/v1/jobs?limit=20')
|
apiJSON<JobsResponse>('/v1/jobs?limit=20')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
healthy = h.ok;
|
const firstReject = [m, p, s, r, j].find((x) => x.status === 'rejected');
|
||||||
moduleItems = m.items ?? [];
|
if (firstReject?.status === 'rejected') {
|
||||||
modulesHasMore = m.has_more;
|
loadError = toErrorMessage(firstReject.reason);
|
||||||
peerItems = p.items ?? [];
|
notifyApiError(firstReject.reason);
|
||||||
peersHasMore = p.has_more;
|
}
|
||||||
speakerItems = s.items ?? [];
|
|
||||||
speakersHasMore = s.has_more;
|
if (m.status === 'fulfilled') {
|
||||||
revisionItems = r.items ?? [];
|
moduleItems = m.value.items ?? [];
|
||||||
revisionsHasMore = r.has_more;
|
modulesHasMore = m.value.has_more;
|
||||||
jobItems = j.items ?? [];
|
}
|
||||||
recentJobs = jobItems.slice(0, 10);
|
if (p.status === 'fulfilled') {
|
||||||
recentRevisions = revisionItems.slice(0, 10);
|
peerItems = p.value.items ?? [];
|
||||||
runningJobs = jobItems.filter((i) => i.status === 'running' || i.status === 'queued').length;
|
peersHasMore = p.value.has_more;
|
||||||
lastUpdated = new Date();
|
}
|
||||||
} catch (e) {
|
if (s.status === 'fulfilled') {
|
||||||
healthy = false;
|
speakerItems = s.value.items ?? [];
|
||||||
loadError = e instanceof Error ? e.message : String(e);
|
speakersHasMore = s.value.has_more;
|
||||||
notifyApiError(e);
|
}
|
||||||
|
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 {
|
} finally {
|
||||||
initialLoading = false;
|
initialLoading = false;
|
||||||
refreshing = false;
|
refreshing = false;
|
||||||
@@ -235,18 +266,30 @@
|
|||||||
<AlertTitle>Проверка API…</AlertTitle>
|
<AlertTitle>Проверка API…</AlertTitle>
|
||||||
<AlertDescription>Запрос к <code class="text-xs">/v1/health</code></AlertDescription>
|
<AlertDescription>Запрос к <code class="text-xs">/v1/health</code></AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
{:else if healthy}
|
{:else if healthy && !loadError}
|
||||||
<Alert class="border-success/30 bg-success/5">
|
<Alert class="border-success/30 bg-success/5">
|
||||||
<CheckCircle class="text-success" />
|
<CheckCircle class="text-success" />
|
||||||
<AlertTitle>API работает</AlertTitle>
|
<AlertTitle>API работает</AlertTitle>
|
||||||
<AlertDescription>Сервер отвечает на запросы health-check.</AlertDescription>
|
<AlertDescription>Сервер отвечает на запросы health-check.</AlertDescription>
|
||||||
</Alert>
|
</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}
|
{:else}
|
||||||
<Alert variant="destructive" class="border-destructive/30 bg-destructive/5">
|
<Alert variant="destructive" class="border-destructive/30 bg-destructive/5">
|
||||||
<XCircle class="text-destructive" />
|
<XCircle class="text-destructive" />
|
||||||
<AlertTitle>API недоступен</AlertTitle>
|
<AlertTitle>API недоступен</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
Не удалось получить ответ от сервера. Проверьте подключение и статус API.
|
{loadError ??
|
||||||
|
'Не удалось получить ответ от сервера. Проверьте, что API запущен (порт 8080), в dev — `npm run dev` с прокси Vite, в Docker — контейнер evobgp-api / evobgp-all и nginx в evobgp-web.'}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user