diff --git a/internal/jobs/worker.go b/internal/jobs/worker.go
index 0131b7c..cede83c 100644
--- a/internal/jobs/worker.go
+++ b/internal/jobs/worker.go
@@ -58,12 +58,13 @@ type Worker struct {
}
type revisionLogEntry struct {
- Kind string `json:"kind"`
- Source string `json:"source"`
- Community string `json:"community"`
- PrefixCount int `json:"prefix_count"`
- Sample []string `json:"sample,omitempty"`
- Message string `json:"message"`
+ Kind string `json:"kind"`
+ Source string `json:"source"`
+ Community string `json:"community"`
+ CommunityLabel string `json:"community_label"`
+ PrefixCount int `json:"prefix_count"`
+ Sample []string `json:"sample,omitempty"`
+ Message string `json:"message"`
}
var defaultWorkerHTTP = &http.Client{Timeout: 45 * time.Second}
@@ -275,17 +276,54 @@ func (w *Worker) runRollback(j *Job) {
"rollback_summary": map[string]any{
"source_revision_id": src,
"new_revision_id": newID,
- "message": fmt.Sprintf("Rollback %s -> %s", shortID(src), shortID(newID)),
+ "message": fmt.Sprintf("Откат %s → %s", shortID(src), shortID(newID)),
},
})
w.enqueueDeployAllSpeakers(j, j.TenantID, newID)
j.Succeed()
}
+// buildCommunityLabelMap maps community UUID -> human-readable title (or BGP community string).
+func buildCommunityLabelMap(st store.Backend, tenantID string) map[string]string {
+ out := make(map[string]string)
+ if st == nil {
+ return out
+ }
+ list, err := st.ListCommunities(tenantID)
+ if err != nil || list == nil {
+ return out
+ }
+ for _, c := range list {
+ if c == nil {
+ continue
+ }
+ label := strings.TrimSpace(c.Title)
+ if label == "" {
+ label = strings.TrimSpace(c.Community)
+ }
+ if label == "" {
+ label = c.ID
+ }
+ out[c.ID] = label
+ }
+ return out
+}
+
+func resolveCommunityLabel(commID string, byID map[string]string) string {
+ if commID == "" || commID == "none" {
+ return "без community"
+ }
+ if lbl, ok := byID[commID]; ok && strings.TrimSpace(lbl) != "" {
+ return strings.TrimSpace(lbl)
+ }
+ return commID
+}
+
func (w *Worker) buildRevisionLogEntries(tenantID, revID string) ([]map[string]any, int, error) {
if w == nil || w.Store == nil {
return nil, 0, fmt.Errorf("store not configured")
}
+ commLabels := buildCommunityLabelMap(w.Store, tenantID)
var all []store.PrefixRow
cursor := ""
for {
@@ -333,14 +371,16 @@ func (w *Worker) buildRevisionLogEntries(tenantID, revID string) ([]map[string]a
out := make([]map[string]any, 0, len(keys))
for _, k := range keys {
g := groups[k]
- msg := humanLogMessage(g.kind, g.source, g.count, g.community, g.sample)
+ cl := resolveCommunityLabel(g.community, commLabels)
+ msg := humanLogMessage(g.kind, g.source, g.count, cl, g.sample)
out = append(out, map[string]any{
- "kind": g.kind,
- "source": g.source,
- "community": g.community,
- "prefix_count": g.count,
- "sample": g.sample,
- "message": msg,
+ "kind": g.kind,
+ "source": g.source,
+ "community": g.community,
+ "community_label": cl,
+ "prefix_count": g.count,
+ "sample": g.sample,
+ "message": msg,
})
}
return out, len(all), nil
@@ -364,22 +404,23 @@ func classifySource(src string) (kind, name string) {
}
}
-func humanLogMessage(kind, source string, count int, community string, sample []string) string {
+// humanLogMessage builds a Russian log line; communityLabel is already resolved (title or BGP value).
+func humanLogMessage(kind, source string, count int, communityLabel string, sample []string) string {
switch kind {
case "asn":
- return fmt.Sprintf("AS%s -> %d префиксов добавлены в community %s", source, count, community)
+ return fmt.Sprintf("AS%s: добавлено %d префиксов в сообщество «%s»", source, count, communityLabel)
case "domain":
ips := strings.Join(prettyDomainSample(sample), " ")
if ips == "" {
- ips = "-"
+ ips = "—"
}
- return fmt.Sprintf("%s -> ip (%s) -> добавлены в community %s", source, ips, community)
+ return fmt.Sprintf("%s: IP (%s) → добавлено в сообщество «%s»", source, ips, communityLabel)
case "cdn":
- return fmt.Sprintf("CDN source %s -> %d префиксов добавлены в community %s", source, count, community)
+ return fmt.Sprintf("CDN «%s»: добавлено %d префиксов в сообщество «%s»", source, count, communityLabel)
case "ip_range":
- return fmt.Sprintf("IP ranges -> %d префиксов добавлены в community %s", count, community)
+ return fmt.Sprintf("Статические диапазоны: добавлено %d префиксов в сообщество «%s»", count, communityLabel)
default:
- return fmt.Sprintf("%s -> %d префиксов добавлены в community %s", source, count, community)
+ return fmt.Sprintf("%s: добавлено %d префиксов в сообщество «%s»", source, count, communityLabel)
}
}
diff --git a/web/src/lib/AppShell.svelte b/web/src/lib/AppShell.svelte
index f826e85..52e13eb 100644
--- a/web/src/lib/AppShell.svelte
+++ b/web/src/lib/AppShell.svelte
@@ -70,7 +70,7 @@
{#if !collapsed}
-
+
{/if}
@@ -133,7 +133,8 @@
href={resolve(item.href)}
class={cn(
buttonVariants({ variant: active ? 'secondary' : 'ghost', size: 'icon-sm' }),
- 'w-full no-underline flex items-center justify-center'
+ 'w-full no-underline flex items-center justify-center',
+ active && 'ring-sidebar-primary/50 bg-sidebar-accent/90 text-sidebar-accent-foreground ring-1'
)}
aria-current={active ? 'page' : undefined}
>
@@ -147,7 +148,9 @@
href={resolve(item.href)}
class={cn(
buttonVariants({ variant: active ? 'secondary' : 'ghost', size: 'sm' }),
- 'w-full justify-start gap-2 no-underline'
+ 'w-full justify-start gap-2 no-underline',
+ active &&
+ 'border-l-sidebar-primary bg-sidebar-accent/80 text-sidebar-accent-foreground border-l-2 shadow-sm'
)}
aria-current={active ? 'page' : undefined}
>
@@ -171,7 +174,8 @@
href={resolve(item.href)}
class={cn(
buttonVariants({ variant: active ? 'secondary' : 'ghost', size: 'icon-sm' }),
- 'w-full no-underline flex items-center justify-center'
+ 'w-full no-underline flex items-center justify-center',
+ active && 'ring-sidebar-primary/50 bg-sidebar-accent/90 text-sidebar-accent-foreground ring-1'
)}
>
@@ -184,7 +188,9 @@
href={resolve(item.href)}
class={cn(
buttonVariants({ variant: active ? 'secondary' : 'ghost', size: 'sm' }),
- 'w-full justify-start gap-2 no-underline'
+ 'w-full justify-start gap-2 no-underline',
+ active &&
+ 'border-l-sidebar-primary bg-sidebar-accent/80 text-sidebar-accent-foreground border-l-2 shadow-sm'
)}
>
diff --git a/web/src/lib/components/operations/OperationsJobsFilters.svelte b/web/src/lib/components/operations/OperationsJobsFilters.svelte
index d28dafe..86b37aa 100644
--- a/web/src/lib/components/operations/OperationsJobsFilters.svelte
+++ b/web/src/lib/components/operations/OperationsJobsFilters.svelte
@@ -5,6 +5,7 @@
import Filter from '@lucide/svelte/icons/filter';
import Search from '@lucide/svelte/icons/search';
import X from '@lucide/svelte/icons/x';
+ import { jobKindFilterRu, jobStatusRu } from '$lib/ui-labels.js';
type Props = {
searchQ: string;
@@ -42,19 +43,19 @@
const statusOptions: { value: string; label: string }[] = [
{ value: '', label: 'Все статусы' },
- { value: 'queued', label: 'queued' },
- { value: 'running', label: 'running' },
- { value: 'succeeded', label: 'succeeded' },
- { value: 'failed', label: 'failed' },
- { value: 'cancelled', label: 'cancelled' }
+ { value: 'queued', label: jobStatusRu('queued') },
+ { value: 'running', label: jobStatusRu('running') },
+ { value: 'succeeded', label: jobStatusRu('succeeded') },
+ { value: 'failed', label: jobStatusRu('failed') },
+ { value: 'cancelled', label: jobStatusRu('cancelled') }
];
const kindOptions: { value: string; label: string }[] = [
{ value: '', label: 'Все типы' },
- { value: 'module_refresh', label: 'module_refresh' },
- { value: 'deploy_apply', label: 'deploy_apply' },
- { value: 'revision_rollback', label: 'revision_rollback' },
- { value: 'bird_reload', label: 'bird_reload' }
+ { value: 'module_refresh', label: jobKindFilterRu('module_refresh') },
+ { value: 'deploy_apply', label: jobKindFilterRu('deploy_apply') },
+ { value: 'revision_rollback', label: jobKindFilterRu('revision_rollback') },
+ { value: 'bird_reload', label: jobKindFilterRu('bird_reload') }
];
@@ -109,7 +110,7 @@
-
+
-
+
-
error
+
Ошибка
{detailedJob.error}
{/if}
@@ -249,7 +250,7 @@
sourceИсточник
kindТип
- {entry.kind}
+ {logKindRu(entry.kind)}
communityСообщество BGP
- {entry.community}
+ {entry.community_label?.trim() || entry.community}
prefixesПрефиксы
{entry.prefix_count}
@@ -292,7 +294,7 @@
{#if entry.sample && entry.sample.length > 0}
- sample
+ Примеры
@@ -326,7 +328,7 @@
Операции по модулю
{#if jobReport.module}
-
{jobReport.module.type}
+
{moduleTypeRu(jobReport.module.type)}
{jobReport.module.name}
{/if}
{#if jobReport.revisionId}
@@ -377,7 +379,7 @@
class="text-chart-4 flex items-center gap-1 text-[11px] font-medium uppercase"
>
- CDN / IP Range
+ CDN / IP-диапазоны
{jobReport.cdn.length}/{jobReport.ipRanges.length}
@@ -389,7 +391,7 @@
Результат агрегации по типам
{#each jobReport.aggregationByKind as row (`${job.job_id}-agg-${row.kind}`)}
- {row.kind}: {row.prefixCount}
+ {logKindRu(row.kind)}: {row.prefixCount}
{/each}
@@ -425,7 +427,7 @@
/>
-
IP range: итог по статическим диапазонам
+
IP-диапазоны: итог по статическим диапазонам
- Meta (raw JSON)
+ Meta (JSON)
{JSON.stringify(
diff --git a/web/src/lib/components/operations/OperationsQuickActions.svelte b/web/src/lib/components/operations/OperationsQuickActions.svelte
index 9d6071c..d328e04 100644
--- a/web/src/lib/components/operations/OperationsQuickActions.svelte
+++ b/web/src/lib/components/operations/OperationsQuickActions.svelte
@@ -51,13 +51,13 @@
-
Apply all speakers
-
Применить текущую конфигурацию на всех спикерах
+
Применить ко всем спикерам
+
Применить текущую конфигурацию на всех BIRD-спикерах
@@ -76,7 +76,7 @@
-
BIRD Reload
+
Перезагрузка BIRD
Перезагрузить конфигурацию BIRD на всех спикерах
@@ -87,7 +87,7 @@
disabled={reloading}
>
- Reload
+ Перезагрузить
@@ -127,7 +127,7 @@
{birdStatus.bgp_established}
/
{birdStatus.bgp_sessions_total}
- Established / всего
+ установлено / всего
{/if}
{:else}
diff --git a/web/src/lib/components/operations/types.ts b/web/src/lib/components/operations/types.ts
index 2d22fd0..94de3c5 100644
--- a/web/src/lib/components/operations/types.ts
+++ b/web/src/lib/components/operations/types.ts
@@ -4,6 +4,8 @@ export type JobLogEntry = {
kind: string;
source: string;
community: string;
+ /** Человекочитаемое имя из справочника (title или BGP community). */
+ community_label?: string;
prefix_count: number;
sample?: string[];
message: string;
diff --git a/web/src/lib/ui-labels.ts b/web/src/lib/ui-labels.ts
new file mode 100644
index 0000000..f7be516
--- /dev/null
+++ b/web/src/lib/ui-labels.ts
@@ -0,0 +1,69 @@
+/** Русские подписи для enum из API (задачи, модули, логи refresh). */
+
+export function jobStatusRu(status: string): string {
+ switch (status) {
+ case 'queued':
+ return 'В очереди';
+ case 'running':
+ return 'Выполняется';
+ case 'succeeded':
+ return 'Успешно';
+ case 'failed':
+ return 'Ошибка';
+ case 'cancelled':
+ return 'Отменена';
+ default:
+ return status;
+ }
+}
+
+/** Подпись типа задачи для фильтров (значения API те же). */
+export function jobKindFilterRu(kind: string): string {
+ switch (kind) {
+ case 'module_refresh':
+ return 'Обновление модуля';
+ case 'deploy_apply':
+ return 'Применение конфигурации';
+ case 'revision_rollback':
+ return 'Откат ревизии';
+ case 'bird_reload':
+ return 'Перезагрузка BIRD';
+ default:
+ return kind;
+ }
+}
+
+export function moduleTypeRu(type: string): string {
+ switch (type) {
+ case 'AS_PREFIXES':
+ return 'AS (номера)';
+ case 'CDN_CIDRS':
+ return 'CDN CIDR';
+ case 'DOMAINS':
+ return 'Домены';
+ case 'IP_RANGES':
+ return 'IP-диапазоны';
+ default:
+ return type;
+ }
+}
+
+/** Тип строки в логе агрегации префиксов (поле kind). */
+export function logKindRu(kind: string): string {
+ switch (kind) {
+ case 'asn':
+ return 'ASN';
+ case 'domain':
+ return 'Домен';
+ case 'cdn':
+ return 'CDN';
+ case 'ip_range':
+ return 'Статические диапазоны';
+ case 'unknown':
+ return 'Неизвестно';
+ case 'source':
+ return 'Источник';
+ default:
+ return kind;
+ }
+}
diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte
index a86d436..eddd7c5 100644
--- a/web/src/routes/+page.svelte
+++ b/web/src/routes/+page.svelte
@@ -16,6 +16,7 @@ const resolve = (path: string) => path as any;
import Activity from '@lucide/svelte/icons/activity';
import Clock from '@lucide/svelte/icons/clock';
import ArrowRight from '@lucide/svelte/icons/arrow-right';
+ import LayoutDashboard from '@lucide/svelte/icons/layout-dashboard';
let healthy = $state(null);
let modules = $state(0);
@@ -128,9 +129,17 @@ const resolve = (path: string) => path as any;
-
-
Обзор
-
Состояние EvoBGP control plane.
+
+
+
+
+
+
Обзор
+
Состояние панели управления EvoBGP.
+
diff --git a/web/src/routes/directories/+page.svelte b/web/src/routes/directories/+page.svelte
index 56f4b90..14febd1 100644
--- a/web/src/routes/directories/+page.svelte
+++ b/web/src/routes/directories/+page.svelte
@@ -45,6 +45,7 @@
import Pencil from '@lucide/svelte/icons/pencil';
import Trash2 from '@lucide/svelte/icons/trash-2';
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
+ import BookOpen from '@lucide/svelte/icons/book-open';
// --- Communities ---
let communities = $state
([]);
@@ -114,10 +115,10 @@
const body = { ...commForm, title: commForm.title?.trim() || undefined };
if (commEdit) {
await apiMutate(`/v1/communities/${commEdit.id}`, 'PATCH', body);
- toast.success('Community обновлена');
+ toast.success('Запись сообщества обновлена');
} else {
await apiMutate('/v1/communities', 'POST', body);
- toast.success('Community создана');
+ toast.success('Сообщество создано');
}
commDialog = false;
await loadComm();
@@ -183,14 +184,22 @@
-
-
Справочники
-
BGP Communities и DoH-профили для резолвинга доменов.
+
+
+
+
+
+
Справочники
+
Сообщества BGP и DoH-профили для резолвинга доменов.
+
- Communities
+ Сообщества BGP
DoH профили
@@ -199,7 +208,7 @@
- BGP Communities
+ Сообщества BGP
Используются для тегирования префиксов
@@ -213,7 +222,7 @@
- Community
+ Код сообщества
Название
ID
@@ -235,7 +244,7 @@
{:else}
- {commLoading ? 'Загрузка…' : 'Нет communities. Создайте первую.'}
+ {commLoading ? 'Загрузка…' : 'Нет записей. Создайте первую.'}
{/each}
@@ -302,11 +311,11 @@