From 53fd527a6dd93000bba1bf55eafd65652649a662 Mon Sep 17 00:00:00 2001
From: Denozordec
Date: Tue, 7 Apr 2026 00:58:15 +0700
Subject: [PATCH] feat: enhance community label handling and improve UI
localization across operations pages. Introduce community label mapping for
better readability in job logs and update various components to reflect
Russian translations, enhancing user experience and consistency in the
interface.
---
internal/jobs/worker.go | 83 ++++++++++++++-----
web/src/lib/AppShell.svelte | 16 ++--
.../operations/OperationsJobsFilters.svelte | 23 ++---
.../operations/OperationsJobsTab.svelte | 34 ++++----
.../operations/OperationsQuickActions.svelte | 12 +--
web/src/lib/components/operations/types.ts | 2 +
web/src/lib/ui-labels.ts | 69 +++++++++++++++
web/src/routes/+page.svelte | 15 +++-
web/src/routes/directories/+page.svelte | 33 +++++---
web/src/routes/modules/+page.svelte | 18 +++-
.../routes/modules/[moduleId]/+page.svelte | 5 +-
web/src/routes/monitoring/+page.svelte | 25 ++++--
web/src/routes/network/+page.svelte | 15 +++-
web/src/routes/operations/+page.svelte | 59 ++++++++-----
web/src/routes/schedule/+page.svelte | 42 ++++++----
web/src/routes/settings/+page.svelte | 15 +++-
16 files changed, 338 insertions(+), 128 deletions(-)
create mode 100644 web/src/lib/ui-labels.ts
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 @@