img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col", className)}
+ {...restProps}
>
{@render children?.()}
diff --git a/web/src/lib/components/ui/card/index.ts b/web/src/lib/components/ui/card/index.ts
index 318b5bb..4d3fce4 100644
--- a/web/src/lib/components/ui/card/index.ts
+++ b/web/src/lib/components/ui/card/index.ts
@@ -1,14 +1,25 @@
-import Root from './card.svelte';
-import Header from './card-header.svelte';
-import Title from './card-title.svelte';
-import Description from './card-description.svelte';
-import Content from './card-content.svelte';
+import Root from "./card.svelte";
+import Content from "./card-content.svelte";
+import Description from "./card-description.svelte";
+import Footer from "./card-footer.svelte";
+import Header from "./card-header.svelte";
+import Title from "./card-title.svelte";
+import Action from "./card-action.svelte";
export {
Root,
+ Content,
+ Description,
+ Footer,
+ Header,
+ Title,
+ Action,
+ //
Root as Card,
+ Content as CardContent,
+ Description as CardDescription,
+ Footer as CardFooter,
Header as CardHeader,
Title as CardTitle,
- Description as CardDescription,
- Content as CardContent
+ Action as CardAction,
};
diff --git a/web/src/lib/components/ui/dialog/dialog-close.svelte b/web/src/lib/components/ui/dialog/dialog-close.svelte
new file mode 100644
index 0000000..de68f2f
--- /dev/null
+++ b/web/src/lib/components/ui/dialog/dialog-close.svelte
@@ -0,0 +1,11 @@
+
+
+
-
+
+
+ {@render children?.()}
+
diff --git a/web/src/lib/components/ui/tabs/index.ts b/web/src/lib/components/ui/tabs/index.ts
new file mode 100644
index 0000000..31267e5
--- /dev/null
+++ b/web/src/lib/components/ui/tabs/index.ts
@@ -0,0 +1,18 @@
+import Root from "./tabs.svelte";
+import Content from "./tabs-content.svelte";
+import List, { tabsListVariants, type TabsListVariant } from "./tabs-list.svelte";
+import Trigger from "./tabs-trigger.svelte";
+
+export {
+ Root,
+ Content,
+ List,
+ Trigger,
+ tabsListVariants,
+ type TabsListVariant,
+ //
+ Root as Tabs,
+ Content as TabsContent,
+ List as TabsList,
+ Trigger as TabsTrigger,
+};
diff --git a/web/src/lib/components/ui/tabs/tabs-content.svelte b/web/src/lib/components/ui/tabs/tabs-content.svelte
new file mode 100644
index 0000000..394ab3e
--- /dev/null
+++ b/web/src/lib/components/ui/tabs/tabs-content.svelte
@@ -0,0 +1,17 @@
+
+
+
diff --git a/web/src/lib/components/ui/tabs/tabs-list.svelte b/web/src/lib/components/ui/tabs/tabs-list.svelte
new file mode 100644
index 0000000..18dce00
--- /dev/null
+++ b/web/src/lib/components/ui/tabs/tabs-list.svelte
@@ -0,0 +1,40 @@
+
+
+
+
+
diff --git a/web/src/lib/components/ui/tabs/tabs-trigger.svelte b/web/src/lib/components/ui/tabs/tabs-trigger.svelte
new file mode 100644
index 0000000..8a0be4a
--- /dev/null
+++ b/web/src/lib/components/ui/tabs/tabs-trigger.svelte
@@ -0,0 +1,23 @@
+
+
+
diff --git a/web/src/lib/components/ui/tabs/tabs.svelte b/web/src/lib/components/ui/tabs/tabs.svelte
new file mode 100644
index 0000000..bfb900d
--- /dev/null
+++ b/web/src/lib/components/ui/tabs/tabs.svelte
@@ -0,0 +1,19 @@
+
+
+
diff --git a/web/src/lib/components/ui/textarea/index.ts b/web/src/lib/components/ui/textarea/index.ts
new file mode 100644
index 0000000..ace797a
--- /dev/null
+++ b/web/src/lib/components/ui/textarea/index.ts
@@ -0,0 +1,7 @@
+import Root from "./textarea.svelte";
+
+export {
+ Root,
+ //
+ Root as Textarea,
+};
diff --git a/web/src/lib/components/ui/textarea/textarea.svelte b/web/src/lib/components/ui/textarea/textarea.svelte
new file mode 100644
index 0000000..2e779ff
--- /dev/null
+++ b/web/src/lib/components/ui/textarea/textarea.svelte
@@ -0,0 +1,23 @@
+
+
+
diff --git a/web/src/lib/components/ui/tooltip/index.ts b/web/src/lib/components/ui/tooltip/index.ts
new file mode 100644
index 0000000..1718604
--- /dev/null
+++ b/web/src/lib/components/ui/tooltip/index.ts
@@ -0,0 +1,19 @@
+import Root from "./tooltip.svelte";
+import Trigger from "./tooltip-trigger.svelte";
+import Content from "./tooltip-content.svelte";
+import Provider from "./tooltip-provider.svelte";
+import Portal from "./tooltip-portal.svelte";
+
+export {
+ Root,
+ Trigger,
+ Content,
+ Provider,
+ Portal,
+ //
+ Root as Tooltip,
+ Content as TooltipContent,
+ Trigger as TooltipTrigger,
+ Provider as TooltipProvider,
+ Portal as TooltipPortal,
+};
diff --git a/web/src/lib/components/ui/tooltip/tooltip-content.svelte b/web/src/lib/components/ui/tooltip/tooltip-content.svelte
new file mode 100644
index 0000000..0cf0694
--- /dev/null
+++ b/web/src/lib/components/ui/tooltip/tooltip-content.svelte
@@ -0,0 +1,52 @@
+
+
+
+
+ {@render children?.()}
+
+ {#snippet child({ props })}
+
+ {/snippet}
+
+
+
diff --git a/web/src/lib/components/ui/tooltip/tooltip-portal.svelte b/web/src/lib/components/ui/tooltip/tooltip-portal.svelte
new file mode 100644
index 0000000..d234f7d
--- /dev/null
+++ b/web/src/lib/components/ui/tooltip/tooltip-portal.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/web/src/lib/components/ui/tooltip/tooltip-provider.svelte b/web/src/lib/components/ui/tooltip/tooltip-provider.svelte
new file mode 100644
index 0000000..6dba9a6
--- /dev/null
+++ b/web/src/lib/components/ui/tooltip/tooltip-provider.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/web/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/web/src/lib/components/ui/tooltip/tooltip-trigger.svelte
new file mode 100644
index 0000000..1acdaa4
--- /dev/null
+++ b/web/src/lib/components/ui/tooltip/tooltip-trigger.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/web/src/lib/components/ui/tooltip/tooltip.svelte b/web/src/lib/components/ui/tooltip/tooltip.svelte
new file mode 100644
index 0000000..44bee9f
--- /dev/null
+++ b/web/src/lib/components/ui/tooltip/tooltip.svelte
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte
index bc28a43..887395f 100644
--- a/web/src/routes/+page.svelte
+++ b/web/src/routes/+page.svelte
@@ -1,97 +1,121 @@
-
+
Обзор
-
- Краткая сводка по API. Укажите токен в разделе «Настройки», если запросы к защищённым путям
- возвращают 401.
-
+
Состояние EvoBGP control plane.
-
-
-
- Health
- GET /v1/health
-
-
- {#if health === 'ok'}
- ok
- {:else if health === 'err'}
- недоступно
- {:else}
- проверка…
- {/if}
-
-
+
+
+
+ {#if healthy === null}
+
+ Проверка…
+ {:else if healthy}
+
+ API работает
+ {:else}
+
+ API недоступен
+ {/if}
+
+
-
-
- Модули
- GET /v1/modules
-
-
- {modulesN}
-
-
-
-
-
- Ревизии
- GET /v1/revisions
-
-
- {revN}
-
-
-
-
-
- Пиры
- GET /v1/peers
-
-
- {peersN}
-
-
+
+
+ {#each stats as stat (stat.label)}
+ {@const Icon = stat.icon}
+
+
+
+
+
+ {stat.label}
+
+
+
+
+ {loading ? '—' : stat.value}
+
+
+
+ {stat.description}
+
+
+ {/each}
+
+
+
+
+ Быстрые действия
+
+
+
+
+
+
+
+
+
diff --git a/web/src/routes/directories/+page.svelte b/web/src/routes/directories/+page.svelte
new file mode 100644
index 0000000..3d0779a
--- /dev/null
+++ b/web/src/routes/directories/+page.svelte
@@ -0,0 +1,372 @@
+
+
+
+
+
Справочники
+
BGP Communities и DoH-профили для резолвинга доменов.
+
+
+
+
+ Communities
+ DoH профили
+
+
+
+
+
+
+
+ BGP Communities
+ Используются для тегирования префиксов
+
+
+
+
+
+
+
+ Название
+ Вид
+ ID
+
+
+
+
+ {#each communities as c (c.id)}
+
+ {c.name}
+
+ {#if c.kind}
+ {c.kind}
+ {:else}
+ —
+ {/if}
+
+ {c.id}
+
+
+
+
+
+
+
+ {:else}
+
+
+ {commLoading ? 'Загрузка…' : 'Нет communities. Создайте первую.'}
+
+
+ {/each}
+
+
+
+
+
+
+
+
+
+
+
+ DoH профили
+ DNS-over-HTTPS серверы для резолвинга доменных модулей
+
+
+
+
+
+
+
+ URL
+ Таймаут (мс)
+ ID
+
+
+
+
+ {#each dohProfiles as d (d.id)}
+
+ {d.url}
+ {d.timeout_ms ?? '—'}
+ {d.id}
+
+
+
+
+
+
+
+ {:else}
+
+
+ {dohLoading ? 'Загрузка…' : 'Нет DoH профилей.'}
+
+
+ {/each}
+
+
+
+
+
+
+
+
+
+
+
+
{ if (!v) commDeleteTarget = null; }}>
+
+
+ Удалить community «{commDeleteTarget?.name}»?
+ Это приведёт к удалению привязки во всех модулях.
+
+
+ (commDeleteTarget = null)}>Отмена
+ Удалить
+
+
+
+
+
+
+
+
{ if (!v) dohDeleteTarget = null; }}>
+
+
+ Удалить DoH профиль?
+ {dohDeleteTarget?.url}
+
+
+ (dohDeleteTarget = null)}>Отмена
+ Удалить
+
+
+
diff --git a/web/src/routes/modules/+page.svelte b/web/src/routes/modules/+page.svelte
index 0f5cd5a..2ea48db 100644
--- a/web/src/routes/modules/+page.svelte
+++ b/web/src/routes/modules/+page.svelte
@@ -1,10 +1,27 @@
-
-
-
Модули префиксов
-
Список экземпляров модулей tenant (из API).
+
+
+
+
Модули префиксов
+
Управление модулями — AS, CDN, домены, IP-диапазоны.
+
+
+
+
+
-
- Модули
- GET /v1/modules
-
-
- {#if err}
- {err}
- {:else}
-
-
-
-
- Имя
- Тип
- Приоритет
- Статус
-
-
-
- {#each rows as m}
-
- {m.name}
-
- {m.type}
-
- {m.priority}
-
- {#if m.enabled}
- вкл
- {:else}
- выкл
- {/if}
-
-
- {:else}
-
- Нет данных
-
- {/each}
-
-
-
- {/if}
+
+
+
+
+ Название
+ Тип
+ Приоритет
+ Интервал
+ Статус
+
+
+
+
+ {#each rows as m (m.id)}
+
+ {m.name}
+
+ {m.type}
+
+ {m.priority}
+
+ {m.cron_expr ?? (m.refresh_interval_sec ? `${m.refresh_interval_sec}с` : '—')}
+
+
+ {#if m.enabled}
+ вкл
+ {:else}
+ выкл
+ {/if}
+
+
+
+
+
+ {:else}
+
+
+ {loading ? 'Загрузка…' : 'Нет модулей. Создайте первый.'}
+
+
+ {/each}
+
+
+
+
+
diff --git a/web/src/routes/modules/[moduleId]/+page.svelte b/web/src/routes/modules/[moduleId]/+page.svelte
new file mode 100644
index 0000000..e8a70e1
--- /dev/null
+++ b/web/src/routes/modules/[moduleId]/+page.svelte
@@ -0,0 +1,909 @@
+
+
+{#if loadingMod}
+
Загрузка…
+{:else if mod}
+
+
+
+
+
+
+
+
{mod.name}
+ {mod.type}
+ {#if mod.enabled}
+ вкл
+ {:else}
+ выкл
+ {/if}
+
+
{mod.id}
+
+
+
+
+
+
+
+
+
+
+
+
+ Приоритет
+ {mod.priority}
+
+
+ Интервал
+
+ {mod.cron_expr ?? (mod.refresh_interval_sec ? `${mod.refresh_interval_sec}с` : '—')}
+
+
+
+ DoH профиль
+ {mod.doh_profile_id ? mod.doh_profile_id.slice(0, 8) + '…' : '—'}
+
+
+ Community по умолч.
+ {communityName(mod.default_community_id)}
+
+
+
+
+ {#if mod.type === 'AS_PREFIXES'}
+
+
+
+ AS-записи
+ ASN и/или префиксы для анонса
+
+
+
+
+
+
+
+ ASN
+ Префикс
+ Community
+
+
+
+
+ {#each asEntries as entry (entry.id)}
+
+ {entry.asn ?? '—'}
+ {entry.prefix ?? '—'}
+ {communityName(entry.community_id)}
+
+
+
+
+
+
+
+ {:else}
+
+ Нет записей
+
+ {/each}
+
+
+
+
+ {:else if mod.type === 'CDN_CIDRS'}
+
+
+
+ CDN-источники
+ URL источников для скачивания списков CIDR
+
+
+
+
+
+
+
+ URL
+ Тип
+ Community
+ Интервал
+
+
+
+
+ {#each cdnSources as src (src.id)}
+
+ {src.url}
+ {src.source_kind}
+ {communityName(src.community_id)}
+ {src.refresh_interval_sec ? `${src.refresh_interval_sec}с` : '—'}
+
+
+
+
+
+
+
+ {:else}
+
+ Нет источников
+
+ {/each}
+
+
+
+
+ {:else if mod.type === 'DOMAINS'}
+
+
+
+ Домены
+ FQDN для резолвинга через DoH
+
+
+
+
+
+
+
+ FQDN
+ Community
+
+
+
+
+ {#each domainEntries as entry (entry.id)}
+
+ {entry.fqdn}
+ {communityName(entry.community_id)}
+
+
+
+
+
+
+
+ {:else}
+
+ Нет доменов
+
+ {/each}
+
+
+
+
+ {:else if mod.type === 'IP_RANGES'}
+
+
+
+ IP-диапазоны
+ Статические CIDR для анонса
+
+
+
+
+
+
+
+ Префикс (CIDR)
+ Community
+
+
+
+
+ {#each ipEntries as entry (entry.id)}
+
+ {entry.prefix}
+ {communityName(entry.community_id)}
+
+
+
+
+
+
+
+ {:else}
+
+ Нет диапазонов
+
+ {/each}
+
+
+
+
+ {/if}
+
+{:else}
+
Модуль не найден
+{/if}
+
+
+
+
+
+
+
+
+ Удалить модуль «{mod?.name}»?
+ Это действие необратимо. Модуль и все его записи будут удалены.
+
+
+ Отмена
+
+ {deletingMod ? 'Удаление…' : 'Удалить'}
+
+
+
+
+
+
+
+
+
{ if (!v) asDeleteTarget = null; }}>
+
+
+ Удалить запись?
+ ASN: {asDeleteTarget?.asn ?? '—'}, Префикс: {asDeleteTarget?.prefix ?? '—'}
+
+
+ (asDeleteTarget = null)}>Отмена
+ Удалить
+
+
+
+
+
+
+
+
{ if (!v) cdnDeleteTarget = null; }}>
+
+
+ Удалить CDN-источник?
+ {cdnDeleteTarget?.url}
+
+
+ (cdnDeleteTarget = null)}>Отмена
+ Удалить
+
+
+
+
+
+
+
+
{ if (!v) domainDeleteTarget = null; }}>
+
+
+ Удалить домен?
+ {domainDeleteTarget?.fqdn}
+
+
+ (domainDeleteTarget = null)}>Отмена
+ Удалить
+
+
+
+
+
+
+
+
{ if (!v) ipDeleteTarget = null; }}>
+
+
+ Удалить диапазон?
+ {ipDeleteTarget?.prefix}
+
+
+ (ipDeleteTarget = null)}>Отмена
+ Удалить
+
+
+
diff --git a/web/src/routes/modules/[moduleId]/+page.ts b/web/src/routes/modules/[moduleId]/+page.ts
new file mode 100644
index 0000000..d43d0cd
--- /dev/null
+++ b/web/src/routes/modules/[moduleId]/+page.ts
@@ -0,0 +1 @@
+export const prerender = false;
diff --git a/web/src/routes/monitoring/+page.svelte b/web/src/routes/monitoring/+page.svelte
index 75d6e72..f23c967 100644
--- a/web/src/routes/monitoring/+page.svelte
+++ b/web/src/routes/monitoring/+page.svelte
@@ -1,148 +1,122 @@
-
-
Мониторинг
-
- Публичные системные эндпоинты и срез Prometheus-метрик (первые строки). Полный scrape обычно делает
- Prometheus.
-
+
+
+
Мониторинг
+
Доступность API и версия сборки.
+
+
-
+
+
- Health
+ Liveness
GET /v1/health
-
- {#if healthOk === true}
- OK
- {:else if healthOk === false}
- ошибка
+
+ {#if health === null}
+ —
+ {:else if health.ok}
+ OK
+ {#if health.status}{health.status}
{/if}
{:else}
- …
+ FAIL
{/if}
- {healthBody || '—'}
+
- Ready
+ Readiness
GET /v1/ready
- {#if ready}
- {JSON.stringify(
- ready,
- null,
- 2
- )}
+ {#if ready === null}
+ —
{:else}
- Недоступно
+ {ready.status}
+ {#if ready.checks}
+
+ {#each Object.entries(ready.checks) as [k, v]}
+
+ {k}
+ {JSON.stringify(v)}
+
+ {/each}
+
+ {/if}
{/if}
+
Версия
GET /v1/version
- {#if version}
-
-
-
- api_version
- - {version.api_version ?? '—'}
-
-
-
- git_sha
- - {version.git_sha ?? '—'}
-
-
+ {#if version === null}
+ —
{:else}
- Недоступно
+
+ {#each Object.entries(version) as [k, v]}
+
+ {k}
+ {String(v)}
+
+ {/each}
+
{/if}
-
-
-
- Метрики
- GET /metrics (Prometheus)
-
-
-
- {#if metricsErr}
- {metricsErr}
- {/if}
-
- {metricsSnippet ||
- 'Нажмите «Загрузить срез»'}
-
-
-
diff --git a/web/src/routes/network/+page.svelte b/web/src/routes/network/+page.svelte
new file mode 100644
index 0000000..1106522
--- /dev/null
+++ b/web/src/routes/network/+page.svelte
@@ -0,0 +1,372 @@
+
+
+
+
+
Сеть
+
BGP-пиры и спикеры (BIRD-агенты).
+
+
+
+
+ Пиры
+ Спикеры
+
+
+
+
+
+
+
+ BGP-пиры
+ Настройка BGP-соседей
+
+
+
+
+
+
+
+ Адрес
+ Remote ASN
+ Состояние сессии
+ Спикер
+
+
+
+
+ {#each peers as p (p.id)}
+
+ {p.neighbor}
+ {p.remote_asn ?? '—'}
+
+ {p.session_state || '—'}
+
+ {p.bgp_speaker_id ? p.bgp_speaker_id.slice(0, 8) + '…' : '—'}
+
+
+
+
+
+
+
+ {:else}
+
+
+ {peersLoading ? 'Загрузка…' : 'Нет пиров.'}
+
+
+ {/each}
+
+
+
+
+
+
+
+
+
+
+
+ Спикеры
+ BIRD-агенты, применяющие конфигурацию
+
+
+
+
+
+
+
+ Endpoint
+ Роль
+ Последняя ревизия
+
+
+
+
+ {#each speakers as s (s.id)}
+
+ {s.endpoint}
+ {s.role}
+ {s.last_applied_revision_id ? s.last_applied_revision_id.slice(0, 8) + '…' : '—'}
+
+
+
+
+
+
+
+ {:else}
+
+
+ {speakersLoading ? 'Загрузка…' : 'Нет спикеров.'}
+
+
+ {/each}
+
+
+
+
+
+
+
+
+
+
+
+
{ if (!v) peerDeleteTarget = null; }}>
+
+
+ Удалить пира?
+ {peerDeleteTarget?.neighbor}
+
+
+ (peerDeleteTarget = null)}>Отмена
+ Удалить
+
+
+
+
+
+
diff --git a/web/src/routes/operations/+page.svelte b/web/src/routes/operations/+page.svelte
new file mode 100644
index 0000000..8e22f20
--- /dev/null
+++ b/web/src/routes/operations/+page.svelte
@@ -0,0 +1,528 @@
+
+
+
+
+
Операции
+
Деплой конфигурации, управление ревизиями и задачами.
+
+
+
+
+
+
+
+
Apply all speakers
+
Применить текущую конфигурацию на всех спикерах
+
+
+
+
+
+
+
+
BIRD Reload
+
Перезагрузить конфигурацию BIRD на всех спикерах
+
+
+
+
+
+
+
+
+ Ревизии
+ Diff
+ Задачи
+
+
+
+
+
+
+
+ История ревизий
+ Автоматически создаются при apply
+
+
+
+
+
+
+
+ ID
+ Создана
+ Префиксов
+ Хэш
+
+
+
+
+ {#each revisions as rev (rev.id)}
+
+ {rev.id.slice(0, 8)}…
+ {formatDate(rev.created_at)}
+ {rev.materialized_prefix_count}
+ {rev.content_hash.slice(0, 12)}…
+
+
+
+
+
+
+
+ {:else}
+
+
+ {revLoading ? 'Загрузка…' : 'Нет ревизий'}
+
+
+ {/each}
+
+
+
+
+
+
+
+
+
+
+ Сравнение ревизий
+ Выберите ID двух ревизий для сравнения
+
+
+
+
+
+
+
+ {#if diffData}
+
+
+
+ Добавлено ({diffData.added?.length ?? 0})
+
+ {#each diffData.added ?? [] as p}
+ {typeof p === 'string' ? p : (p as {prefix?: string}).prefix ?? JSON.stringify(p)}
+ {:else}
+ Нет изменений
+ {/each}
+
+
+
+
- Удалено ({diffData.removed?.length ?? 0})
+
+ {#each diffData.removed ?? [] as p}
+ {typeof p === 'string' ? p : (p as {prefix?: string}).prefix ?? JSON.stringify(p)}
+ {:else}
+ Нет изменений
+ {/each}
+
+
+
+ {/if}
+
+
+
+
+
+
+
+
+
+ Задачи
+ Фоновые задачи (ingest, apply, refresh)
+
+
+
+
+
+
+
+ Вид
+ Статус
+ Создана
+ Завершена
+
+
+
+
+ {#each jobs as job (job.job_id)}
+ openJobDetail(job)}>
+ {job.kind}
+ {job.status}
+ {formatDate(job.created_at)}
+ {formatDate(job.finished_at)}
+
+ {#if job.status === 'running' || job.status === 'queued'}
+
+ {/if}
+
+
+ {:else}
+
+
+ {jobsLoading ? 'Загрузка…' : 'Нет задач'}
+
+
+ {/each}
+
+
+
+
+
+
+
+
+
+
+
+
+ Запустить Apply на всех спикерах?
+ Текущая конфигурация будет применена на всех BIRD-агентах. Требуется роль operator.
+
+
+ Отмена
+ {applying ? 'Apply…' : 'Применить'}
+
+
+
+
+
+
+
+
+ Reload BIRD?
+ BIRD перезагрузит конфигурацию. Требуется роль operator.
+
+
+ Отмена
+ {reloading ? 'Reload…' : 'Reload'}
+
+
+
+
+
+
{ if (!v) rollbackTarget = null; }}>
+
+
+ Откатиться к ревизии {rollbackTarget?.id.slice(0, 8)}…?
+ Будет создана новая ревизия на основе выбранной. Требуется роль operator.
+
+
+ (rollbackTarget = null)}>Отмена
+ {rollingBack ? 'Откат…' : 'Откатить'}
+
+
+
+
+
+
{ if (!v) cancelTarget = null; }}>
+
+
+ Отменить задачу?
+ Задача: {cancelTarget?.kind} ({cancelTarget?.job_id?.slice(0, 8)}…)
+
+
+ (cancelTarget = null)}>Нет
+ {cancelling ? 'Отмена…' : 'Отменить'}
+
+
+
+
+
+
+
+
+
diff --git a/web/src/routes/peers/+page.svelte b/web/src/routes/peers/+page.svelte
index c35cae0..1d74be7 100644
--- a/web/src/routes/peers/+page.svelte
+++ b/web/src/routes/peers/+page.svelte
@@ -1,118 +1,5 @@
-
-
-
-
Пиры и спикеры
-
BGP-пиры и экземпляры BIRD.
-
-
- {#if err}
-
{err}
- {/if}
-
-
-
- Спикеры
- GET /v1/speakers
-
-
-
-
-
-
- Роль
- Endpoint
- Последняя ревизия
-
-
-
- {#each speakers as s}
-
- {s.role}
- {s.endpoint}
- {s.last_applied_revision_id ?? '—'}
-
- {:else}
-
- Нет данных
-
- {/each}
-
-
-
-
-
-
-
-
- Пиры
- GET /v1/peers
-
-
-
-
-
-
- Имя
- Neighbor
- Сессия
-
-
-
- {#each peers as p}
-
- {p.name}
- {p.neighbor}
-
- {p.session_state}
-
-
- {:else}
-
- Нет данных
-
- {/each}
-
-
-
-
-
-
diff --git a/web/src/routes/revisions/+page.svelte b/web/src/routes/revisions/+page.svelte
index 8ca56ff..b7f92f1 100644
--- a/web/src/routes/revisions/+page.svelte
+++ b/web/src/routes/revisions/+page.svelte
@@ -1,80 +1,5 @@
-
-
-
-
Ревизии
-
История конфигурации BIRD.
-
-
-
-
- Ревизии
- GET /v1/revisions
-
-
- {#if err}
- {err}
- {:else}
-
-
-
-
- ID
- Хэш
- Префиксы
- Создана
-
-
-
- {#each rows as r}
-
- {r.id}
- {r.content_hash}
- {r.materialized_prefix_count ?? '—'}
- {r.created_at}
-
- {:else}
-
- Нет данных
-
- {/each}
-
-
-
- {/if}
-
-
-
diff --git a/web/src/routes/schedule/+page.svelte b/web/src/routes/schedule/+page.svelte
index 45debd5..51dad7a 100644
--- a/web/src/routes/schedule/+page.svelte
+++ b/web/src/routes/schedule/+page.svelte
@@ -1,11 +1,10 @@
-
-
Расписание и задачи
-
- Интервалы обновления модулей (поля из API) и ручной refresh → ingest. Ниже — последние задачи из
- job_audit.
-
+
+
+
Расписание и задачи
+
Интервалы обновления модулей и ручной запуск refresh.
+
+
- Модули и refresh
-
- POST /v1/modules/{id}/refresh (роль editor+). CDN/домены/AS — очередь; IP_RANGES — 204.
-
+ Модули
+ Запустить refresh вручную (CDN/домены/AS → очередь; IP_RANGES → 204)
-
-
-
-
+
+
+
+
+ Модуль
+ Тип
+ Интервал
+ Cron
+
+
+
+
+ {#each modules as m (m.id)}
- Модуль
- Тип
- Интервал
- Cron
- Действие
+ {m.name}
+ {m.type}
+ {intervalLabel(m.refresh_interval_sec)}
+ {m.cron_expr || '—'}
+
+
+
-
-
- {#each modules as m}
-
- {m.name}
-
- {m.type}
-
- {intervalLabel(m.refresh_interval_sec)}
- {m.cron_expr || '—'}
-
-
-
-
- {:else}
-
- Нет модулей
-
- {/each}
-
-
-
+ {:else}
+
+
+ {loading ? 'Загрузка…' : 'Нет модулей'}
+
+
+ {/each}
+
+
@@ -129,33 +131,33 @@
Последние задачи
GET /v1/jobs
-
-
-
-
+
+
+
+
+ Вид
+ Статус
+ Создана
+ Ошибка
+
+
+
+ {#each jobs as j (j.job_id)}
- Вид
- Статус
- Создана
+ {j.kind}
+ {j.status}
+ {j.created_at ? new Date(j.created_at).toLocaleString('ru') : '—'}
+ {j.error ?? ''}
-
-
- {#each jobs as j}
-
- {j.kind}
-
- {j.status}
-
- {j.created_at ?? '—'}
-
- {:else}
-
- Нет задач
-
- {/each}
-
-
-
+ {:else}
+
+
+ {loading ? 'Загрузка…' : 'Нет задач'}
+
+
+ {/each}
+
+
diff --git a/web/src/routes/settings/+page.svelte b/web/src/routes/settings/+page.svelte
index bc7159f..0858b0f 100644
--- a/web/src/routes/settings/+page.svelte
+++ b/web/src/routes/settings/+page.svelte
@@ -1,13 +1,22 @@
-
+
Настройки
-
- Bearer-токен для заголовка Authorization. Для
- локального демо с
- EVOBGP_DEV_INSECURE=1
- можно использовать токен dev.
-
+
Токен доступа и параметры API.
+
API-ключ
- Хранится только в localStorage
+
+ Bearer-токен хранится только в localStorage браузера. Для локального демо с
+ EVOBGP_DEV_INSECURE=1
+ используйте токен dev.
+
-
+
+
+
+
+
+
+
+ Настройки системы (API)
+
+ GET/PATCH /v1/settings — глобальные параметры control plane.
+ Требуется роль operator.
+
+
+
+ {#if loadingSettings}
+ Загрузка…
+ {:else if apiSettings !== null}
+
+
+
+
+
+ {:else}
+
+ {/if}