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}
EvoBGP -

Control plane

+

Панель управления

{/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 @@
- + Задачи - Фоновые задачи (ingest, apply, refresh) + Фоновые задачи (ingest, применение, обновление) {#if jobsFetchedTotal !== undefined} · Показано {jobs.length} из {jobsFetchedTotal} @@ -168,7 +169,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" >
@@ -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} >
@@ -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 @@ - {commEdit ? 'Редактировать' : 'Новая'} community + {commEdit ? 'Редактировать сообщество BGP' : 'Новое сообщество BGP'}
- +
@@ -324,7 +333,7 @@ { if (!v) commDeleteTarget = null; }}> - Удалить community «{commDisplay(commDeleteTarget)}»? + Удалить сообщество «{commDisplay(commDeleteTarget)}»? Это приведёт к удалению привязки во всех модулях. diff --git a/web/src/routes/modules/+page.svelte b/web/src/routes/modules/+page.svelte index 12795e3..243048f 100644 --- a/web/src/routes/modules/+page.svelte +++ b/web/src/routes/modules/+page.svelte @@ -46,6 +46,8 @@ import RefreshCw from '@lucide/svelte/icons/refresh-cw'; import ExternalLink from '@lucide/svelte/icons/external-link'; import Trash2 from '@lucide/svelte/icons/trash-2'; + import { moduleTypeRu } from '$lib/ui-labels.js'; + import Boxes from '@lucide/svelte/icons/boxes'; let rows = $state([]); let loading = $state(false); @@ -155,9 +157,17 @@
-
-

Модули префиксов

-

Управление модулями — AS, CDN, домены, IP-диапазоны.

+
+ +
+

Модули префиксов

+

Управление модулями — AS, CDN, домены, IP-диапазоны.

+
@@ -123,13 +135,13 @@ {#each modules as m (m.id)} {m.name} - {m.type} + {moduleTypeRu(m.type)} {intervalLabel(m.refresh_interval_sec)} {m.cron_expr || '—'} @@ -163,8 +175,8 @@ {#each jobs as j (j.job_id)} - {j.kind} - {j.status} + {jobKindFilterRu(j.kind)} + {jobStatusRu(j.status)} {j.created_at ? new Date(j.created_at).toLocaleString('ru') : '—'} {j.error ?? ''} @@ -183,7 +195,7 @@ Операции обновления модулей - Отдельная лента задач `module_refresh` для контроля по модулям. + Отдельная лента задач обновления модулей для контроля по модулям.
@@ -191,19 +203,21 @@ Статус Создана - revision_id + Ревизия {#each refreshJobs as j (j.job_id)} - {j.status} + {jobStatusRu(j.status)} {j.created_at ? new Date(j.created_at).toLocaleString('ru') : '—'} {typeof j.meta?.revision_id === 'string' ? `${j.meta.revision_id.slice(0, 12)}…` : '—'} {:else} - Нет задач module_refresh + Нет задач обновления модулей {/each} diff --git a/web/src/routes/settings/+page.svelte b/web/src/routes/settings/+page.svelte index 1bf1678..dbd993e 100644 --- a/web/src/routes/settings/+page.svelte +++ b/web/src/routes/settings/+page.svelte @@ -11,6 +11,7 @@ import { Textarea } from '$lib/components/ui/textarea/index.js'; import { toast } from 'svelte-sonner'; import Save from '@lucide/svelte/icons/save'; + import SettingsIcon from '@lucide/svelte/icons/settings'; let token = $state(''); let apiSettings = $state(null); @@ -63,9 +64,17 @@
-
-

Настройки

-

Токен доступа и параметры API.

+
+ +
+

Настройки

+

Токен доступа и параметры API.

+