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.

+