Translate UI text to Russian for improved localization across various components and pages, including sidebar, pagination, and server details. Update labels, tooltips, and headers to enhance user experience for Russian-speaking users.
Publish telemt-api gateway Docker image / test (push) Successful in 30s
Publish telemt-api gateway Docker image / build-and-push (push) Successful in 2m29s

This commit is contained in:
Denozordec
2026-04-10 00:17:52 +07:00
parent 297fb79f11
commit 8ad627cb9b
20 changed files with 121 additions and 95 deletions
+4 -4
View File
@@ -4,12 +4,12 @@ const SEG_LABELS: Record<string, string> = {
users: 'Пользователи', users: 'Пользователи',
ips: 'Уникальные IP', ips: 'Уникальные IP',
incidents: 'Инциденты', incidents: 'Инциденты',
live: 'Live', live: 'Поток',
mihomo: 'Mihomo', mihomo: 'Mihomo',
servers: 'Серверы', servers: 'Серверы',
runtime: 'Runtime', runtime: 'Состояние',
security: 'Security', security: 'Безопасность',
upstreams: 'Upstreams', upstreams: 'Апстримы и ДЦ',
config: 'Конфигурация', config: 'Конфигурация',
update: 'Обновление' update: 'Обновление'
}; };
+15 -15
View File
@@ -85,7 +85,7 @@
</Sidebar.MenuButton> </Sidebar.MenuButton>
</Sidebar.MenuItem> </Sidebar.MenuItem>
<Sidebar.MenuItem> <Sidebar.MenuItem>
<Sidebar.MenuButton isActive={active('/incidents')} tooltipContent="Incidents"> <Sidebar.MenuButton isActive={active('/incidents')} tooltipContent="Инциденты">
{#snippet child({ props })} {#snippet child({ props })}
<a href="/incidents" {...props}> <a href="/incidents" {...props}>
<SirenIcon /> <SirenIcon />
@@ -143,11 +143,11 @@
<Sidebar.GroupLabel>Нода</Sidebar.GroupLabel> <Sidebar.GroupLabel>Нода</Sidebar.GroupLabel>
<Sidebar.Menu> <Sidebar.Menu>
<Sidebar.MenuItem> <Sidebar.MenuItem>
<Sidebar.MenuButton isActive={activeExact(serverPath)} tooltipContent="Dashboard"> <Sidebar.MenuButton isActive={activeExact(serverPath)} tooltipContent="Обзор">
{#snippet child({ props })} {#snippet child({ props })}
<a href={serverPath} {...props}> <a href={serverPath} {...props}>
<LayoutDashboardIcon /> <LayoutDashboardIcon />
<span>Dashboard</span> <span>Обзор</span>
</a> </a>
{/snippet} {/snippet}
</Sidebar.MenuButton> </Sidebar.MenuButton>
@@ -163,61 +163,61 @@
</Sidebar.MenuButton> </Sidebar.MenuButton>
</Sidebar.MenuItem> </Sidebar.MenuItem>
<Sidebar.MenuItem> <Sidebar.MenuItem>
<Sidebar.MenuButton isActive={active(serverPath + '/runtime')} tooltipContent="Runtime"> <Sidebar.MenuButton isActive={active(serverPath + '/runtime')} tooltipContent="Состояние">
{#snippet child({ props })} {#snippet child({ props })}
<a href="{serverPath}/runtime" {...props}> <a href="{serverPath}/runtime" {...props}>
<ActivityIcon /> <ActivityIcon />
<span>Runtime</span> <span>Состояние</span>
</a> </a>
{/snippet} {/snippet}
</Sidebar.MenuButton> </Sidebar.MenuButton>
</Sidebar.MenuItem> </Sidebar.MenuItem>
<Sidebar.MenuItem> <Sidebar.MenuItem>
<Sidebar.MenuButton isActive={active(serverPath + '/users')} tooltipContent="Users"> <Sidebar.MenuButton isActive={active(serverPath + '/users')} tooltipContent="Пользователи">
{#snippet child({ props })} {#snippet child({ props })}
<a href="{serverPath}/users" {...props}> <a href="{serverPath}/users" {...props}>
<UsersIcon /> <UsersIcon />
<span>Users</span> <span>Пользователи</span>
</a> </a>
{/snippet} {/snippet}
</Sidebar.MenuButton> </Sidebar.MenuButton>
</Sidebar.MenuItem> </Sidebar.MenuItem>
<Sidebar.MenuItem> <Sidebar.MenuItem>
<Sidebar.MenuButton isActive={active(serverPath + '/security')} tooltipContent="Security"> <Sidebar.MenuButton isActive={active(serverPath + '/security')} tooltipContent="Безопасность">
{#snippet child({ props })} {#snippet child({ props })}
<a href="{serverPath}/security" {...props}> <a href="{serverPath}/security" {...props}>
<ShieldIcon /> <ShieldIcon />
<span>Security</span> <span>Безопасность</span>
</a> </a>
{/snippet} {/snippet}
</Sidebar.MenuButton> </Sidebar.MenuButton>
</Sidebar.MenuItem> </Sidebar.MenuItem>
<Sidebar.MenuItem> <Sidebar.MenuItem>
<Sidebar.MenuButton isActive={active(serverPath + '/upstreams')} tooltipContent="Upstreams"> <Sidebar.MenuButton isActive={active(serverPath + '/upstreams')} tooltipContent="Апстримы и ДЦ">
{#snippet child({ props })} {#snippet child({ props })}
<a href="{serverPath}/upstreams" {...props}> <a href="{serverPath}/upstreams" {...props}>
<NetworkIcon /> <NetworkIcon />
<span>Upstreams & DCs</span> <span>Апстримы и ДЦ</span>
</a> </a>
{/snippet} {/snippet}
</Sidebar.MenuButton> </Sidebar.MenuButton>
</Sidebar.MenuItem> </Sidebar.MenuItem>
<Sidebar.MenuItem> <Sidebar.MenuItem>
<Sidebar.MenuButton isActive={active(serverPath + '/config')} tooltipContent="Configuration"> <Sidebar.MenuButton isActive={active(serverPath + '/config')} tooltipContent="Конфигурация">
{#snippet child({ props })} {#snippet child({ props })}
<a href="{serverPath}/config" {...props}> <a href="{serverPath}/config" {...props}>
<SettingsIcon /> <SettingsIcon />
<span>Configuration</span> <span>Конфигурация</span>
</a> </a>
{/snippet} {/snippet}
</Sidebar.MenuButton> </Sidebar.MenuButton>
</Sidebar.MenuItem> </Sidebar.MenuItem>
<Sidebar.MenuItem> <Sidebar.MenuItem>
<Sidebar.MenuButton isActive={active(serverPath + '/update')} tooltipContent="Update"> <Sidebar.MenuButton isActive={active(serverPath + '/update')} tooltipContent="Обновление">
{#snippet child({ props })} {#snippet child({ props })}
<a href="{serverPath}/update" {...props}> <a href="{serverPath}/update" {...props}>
<RefreshCwIcon /> <RefreshCwIcon />
<span>Update</span> <span>Обновление</span>
</a> </a>
{/snippet} {/snippet}
</Sidebar.MenuButton> </Sidebar.MenuButton>
@@ -13,13 +13,13 @@
</script> </script>
{#snippet Fallback()} {#snippet Fallback()}
<span>Next</span> <span>Следующая</span>
<ChevronRightIcon class="size-4" /> <ChevronRightIcon class="size-4" />
{/snippet} {/snippet}
<PaginationPrimitive.NextButton <PaginationPrimitive.NextButton
bind:ref bind:ref
aria-label="Go to next page" aria-label="Перейти на следующую страницу"
class={cn(buttonVariants({ variant: "ghost" }), "pr-1.5!", className)} class={cn(buttonVariants({ variant: "ghost" }), "pr-1.5!", className)}
{...restProps} {...restProps}
> >
@@ -10,11 +10,11 @@
</script> </script>
<PaginationLink <PaginationLink
aria-label="Go to next page" aria-label="Перейти на следующую страницу"
size="default" size="default"
class={cn("pr-1.5!", className)} class={cn("pr-1.5!", className)}
{...restProps} {...restProps}
> >
<span class="cn-pagination-next-text hidden sm:block">Next</span> <span class="cn-pagination-next-text hidden sm:block">Следующая</span>
<ChevronRightIcon data-icon="inline-end" /> <ChevronRightIcon data-icon="inline-end" />
</PaginationLink> </PaginationLink>
@@ -14,12 +14,12 @@
{#snippet Fallback()} {#snippet Fallback()}
<ChevronLeftIcon class="size-4" /> <ChevronLeftIcon class="size-4" />
<span>Previous</span> <span>Предыдущая</span>
{/snippet} {/snippet}
<PaginationPrimitive.PrevButton <PaginationPrimitive.PrevButton
bind:ref bind:ref
aria-label="Go to previous page" aria-label="Перейти на предыдущую страницу"
class={cn(buttonVariants({ variant: "ghost" }), "pl-1.5!", className)} class={cn(buttonVariants({ variant: "ghost" }), "pl-1.5!", className)}
{...restProps} {...restProps}
> >
@@ -10,11 +10,11 @@
</script> </script>
<PaginationLink <PaginationLink
aria-label="Go to previous page" aria-label="Перейти на предыдущую страницу"
size="default" size="default"
class={cn("pl-1.5!", className)} class={cn("pl-1.5!", className)}
{...restProps} {...restProps}
> >
<ChevronLeftIcon data-icon="inline-start" /> <ChevronLeftIcon data-icon="inline-start" />
<span class="cn-pagination-previous-text hidden sm:block">Previous</span> <span class="cn-pagination-previous-text hidden sm:block">Предыдущая</span>
</PaginationLink> </PaginationLink>
@@ -18,7 +18,7 @@
bind:ref bind:ref
bind:page bind:page
role="navigation" role="navigation"
aria-label="pagination" aria-label="Пагинация"
data-slot="pagination" data-slot="pagination"
{count} {count}
{perPage} {perPage}
@@ -51,8 +51,8 @@
{side} {side}
> >
<Sheet.Header class="sr-only"> <Sheet.Header class="sr-only">
<Sheet.Title>Sidebar</Sheet.Title> <Sheet.Title>Боковая панель</Sheet.Title>
<Sheet.Description>Displays the mobile sidebar.</Sheet.Description> <Sheet.Description>Отображает мобильную боковую панель.</Sheet.Description>
</Sheet.Header> </Sheet.Header>
<div class="flex h-full w-full flex-col"> <div class="flex h-full w-full flex-col">
{@render children?.()} {@render children?.()}
+4 -4
View File
@@ -2,12 +2,12 @@ const SEG_LABELS: Record<string, string> = {
users: 'Пользователи', users: 'Пользователи',
ips: 'Уникальные IP', ips: 'Уникальные IP',
incidents: 'Инциденты', incidents: 'Инциденты',
live: 'Live', live: 'Поток',
mihomo: 'Mihomo', mihomo: 'Mihomo',
servers: 'Серверы', servers: 'Серверы',
runtime: 'Runtime', runtime: 'Состояние',
security: 'Security', security: 'Безопасность',
upstreams: 'Upstreams и DC', upstreams: 'Апстримы и ДЦ',
config: 'Конфигурация', config: 'Конфигурация',
update: 'Обновление' update: 'Обновление'
}; };
+7 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import './layout.css'; import './layout.css';
import { page } from '$app/state'; import { page, navigating } from '$app/state';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { ModeWatcher } from 'mode-watcher'; import { ModeWatcher } from 'mode-watcher';
import favicon from '$lib/assets/favicon.svg'; import favicon from '$lib/assets/favicon.svg';
@@ -15,6 +15,7 @@
let { children } = $props(); let { children } = $props();
const docTitle = $derived(`${panelTitleFromPath(page.url.pathname)} · Telemt Panel`); const docTitle = $derived(`${panelTitleFromPath(page.url.pathname)} · Telemt Panel`);
const isNavigating = $derived(navigating !== null);
let serverAliases = $state<string[]>([]); let serverAliases = $state<string[]>([]);
@@ -54,6 +55,11 @@
<Sidebar.Provider> <Sidebar.Provider>
<AppSidebar {serverAliases} {serverPath} /> <AppSidebar {serverAliases} {serverPath} />
<Sidebar.Inset> <Sidebar.Inset>
{#if isNavigating}
<div class="h-0.5 w-full overflow-hidden bg-transparent">
<div class="h-full w-1/3 animate-pulse rounded-full bg-primary"></div>
</div>
{/if}
<header class="flex h-14 w-full shrink-0 items-center gap-2 border-b border-border px-4"> <header class="flex h-14 w-full shrink-0 items-center gap-2 border-b border-border px-4">
<SiteHeader /> <SiteHeader />
</header> </header>
+1 -1
View File
@@ -572,7 +572,7 @@
{/snippet} {/snippet}
{summary.fleet_total_connections} {summary.fleet_total_connections}
</KpiStatCard> </KpiStatCard>
<KpiStatCard accent="danger" label="Bad connections" footer="сумма по нодам" valueClass="text-xl"> <KpiStatCard accent="danger" label="Плохие соединения" footer="сумма по нодам" valueClass="text-xl">
{#snippet icon()} {#snippet icon()}
<ShieldAlertIcon class="size-5" aria-hidden="true" /> <ShieldAlertIcon class="size-5" aria-hidden="true" />
{/snippet} {/snippet}
+23 -17
View File
@@ -124,6 +124,12 @@
return 'outline'; return 'outline';
} }
function severityLabel(sev: IncidentItem['severity']): string {
if (sev === 'critical') return 'критично';
if (sev === 'warning') return 'предупреждение';
return 'инфо';
}
function restartPolling() { function restartPolling() {
if (pollTimer) { if (pollTimer) {
clearInterval(pollTimer); clearInterval(pollTimer);
@@ -283,7 +289,7 @@
<div class="mb-6 flex flex-wrap items-center justify-between gap-4"> <div class="mb-6 flex flex-wrap items-center justify-between gap-4">
<div> <div>
<h1 class="text-2xl font-semibold tracking-tight">Инциденты</h1> <h1 class="text-2xl font-semibold tracking-tight">Инциденты</h1>
<p class="text-sm text-muted-foreground">Сводка и triage по всему флоту.</p> <p class="text-sm text-muted-foreground">Сводка и разбор инцидентов по всему флоту.</p>
{#if generatedAt} {#if generatedAt}
<p class="mt-1 text-xs text-muted-foreground">Снимок: {new Date(generatedAt).toLocaleString()}</p> <p class="mt-1 text-xs text-muted-foreground">Снимок: {new Date(generatedAt).toLocaleString()}</p>
{/if} {/if}
@@ -331,38 +337,38 @@
{#snippet children()} {#snippet children()}
<div class="mb-4 grid gap-4 sm:grid-cols-3"> <div class="mb-4 grid gap-4 sm:grid-cols-3">
<KpiTrendCard <KpiTrendCard
label="Critical" label="Критичные"
valueDisplay={String(data?.critical_total ?? 0)} valueDisplay={String(data?.critical_total ?? 0)}
prevValue={kpiPrev?.critical ?? null} prevValue={kpiPrev?.critical ?? null}
currentValue={data != null ? (data.critical_total ?? 0) : null} currentValue={data != null ? (data.critical_total ?? 0) : null}
invertTrend={true} invertTrend={true}
insight="Открытые critical; меньше — лучше. К прошлому опросу." insight="Открытые критичные; меньше — лучше. К прошлому опросу."
/> />
<KpiTrendCard <KpiTrendCard
label="Warning" label="Предупреждения"
valueDisplay={String(data?.warning_total ?? 0)} valueDisplay={String(data?.warning_total ?? 0)}
prevValue={kpiPrev?.warning ?? null} prevValue={kpiPrev?.warning ?? null}
currentValue={data != null ? (data.warning_total ?? 0) : null} currentValue={data != null ? (data.warning_total ?? 0) : null}
invertTrend={true} invertTrend={true}
insight="Открытые warning; меньше — лучше." insight="Открытые предупреждения; меньше — лучше."
/> />
<KpiTrendCard <KpiTrendCard
label="Info" label="Инфо"
valueDisplay={String(data?.info_total ?? 0)} valueDisplay={String(data?.info_total ?? 0)}
prevValue={kpiPrev?.info ?? null} prevValue={kpiPrev?.info ?? null}
currentValue={data != null ? (data.info_total ?? 0) : null} currentValue={data != null ? (data.info_total ?? 0) : null}
insight="Открытые info. К прошлому опросу." insight="Открытые инфо. К прошлому опросу."
/> />
</div> </div>
<DataTableCard title="Инциденты" description="Список и triage"> <DataTableCard title="Инциденты" description="Список и разбор">
{#snippet toolbar()} {#snippet toolbar()}
<DataTableToolbar> <DataTableToolbar>
<div class="relative max-w-md flex-1"> <div class="relative max-w-md flex-1">
<SearchIcon <SearchIcon
class="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 text-muted-foreground" class="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 text-muted-foreground"
/> />
<Input class="h-8 ps-8" placeholder="Поиск по заголовку, summary, severity…" bind:value={incSearch} /> <Input class="h-8 ps-8" placeholder="Поиск по заголовку, описанию, критичности…" bind:value={incSearch} />
</div> </div>
<Button <Button
variant="outline" variant="outline"
@@ -384,7 +390,7 @@
active={incSortKey === 'sev'} active={incSortKey === 'sev'}
dir={incSortDir} dir={incSortDir}
onToggle={() => toggleIncSort('sev')} onToggle={() => toggleIncSort('sev')}
>Severity</SortableTh> >Критичность</SortableTh>
<SortableTh <SortableTh
active={incSortKey === 'title'} active={incSortKey === 'title'}
dir={incSortDir} dir={incSortDir}
@@ -406,8 +412,8 @@
</Tooltip.Root> </Tooltip.Root>
</div> </div>
</Table.Head> </Table.Head>
<Table.Head>Runbook</Table.Head> <Table.Head>Инструкции</Table.Head>
<Table.Head>Triage</Table.Head> <Table.Head>Разбор</Table.Head>
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
<Table.Body> <Table.Body>
@@ -427,7 +433,7 @@
{#each filteredIncidents as item (item.id)} {#each filteredIncidents as item (item.id)}
<Table.Row> <Table.Row>
<Table.Cell> <Table.Cell>
<Badge variant={severityVariant(item.severity)}>{item.severity}</Badge> <Badge variant={severityVariant(item.severity)}>{severityLabel(item.severity)}</Badge>
</Table.Cell> </Table.Cell>
<Table.Cell class="max-w-[360px]"> <Table.Cell class="max-w-[360px]">
<div class="font-medium">{item.title}</div> <div class="font-medium">{item.title}</div>
@@ -458,21 +464,21 @@
variant={triage.ack ? 'default' : 'outline'} variant={triage.ack ? 'default' : 'outline'}
onclick={() => toggleAck(item.id)} onclick={() => toggleAck(item.id)}
> >
{triage.ack ? 'unack' : 'ack'} {triage.ack ? 'Снять подтверждение' : 'Подтвердить'}
</Button> </Button>
<Button <Button
size="xs" size="xs"
variant={triage.resolved ? 'default' : 'outline'} variant={triage.resolved ? 'default' : 'outline'}
onclick={() => toggleResolved(item.id)} onclick={() => toggleResolved(item.id)}
> >
{triage.resolved ? 'unresolve' : 'resolve'} {triage.resolved ? 'Вернуть в работу' : 'Решить'}
</Button> </Button>
</div> </div>
<div class="mb-1"> <div class="mb-1">
<Input <Input
type="text" type="text"
class="h-8 w-full text-xs" class="h-8 w-full text-xs"
placeholder="owner" placeholder="Ответственный"
value={triage.owner ?? ''} value={triage.owner ?? ''}
oninput={(e) => oninput={(e) =>
patchTriage(item.id, { patchTriage(item.id, {
@@ -484,7 +490,7 @@
<Input <Input
type="text" type="text"
class="h-8 w-full text-xs" class="h-8 w-full text-xs"
placeholder="note" placeholder="Примечание"
value={triage.note ?? ''} value={triage.note ?? ''}
oninput={(e) => oninput={(e) =>
patchTriage(item.id, { patchTriage(item.id, {
+27 -17
View File
@@ -106,12 +106,24 @@
return 'outline'; return 'outline';
} }
function severityLabel(sev: IncidentSeverity): string {
if (sev === 'critical') return 'критично';
if (sev === 'warning') return 'предупреждение';
return 'инфо';
}
function statusVariant(st: LiveStatus | undefined): 'default' | 'secondary' | 'destructive' { function statusVariant(st: LiveStatus | undefined): 'default' | 'secondary' | 'destructive' {
if (st === 'critical') return 'destructive'; if (st === 'critical') return 'destructive';
if (st === 'degraded') return 'secondary'; if (st === 'degraded') return 'secondary';
return 'default'; return 'default';
} }
function statusLabel(st: LiveStatus | undefined): string {
if (st === 'critical') return 'критично';
if (st === 'degraded') return 'деградация';
return 'норма';
}
function parseAliasFilter(raw: string | null): string { function parseAliasFilter(raw: string | null): string {
const value = raw?.trim() ?? ''; const value = raw?.trim() ?? '';
return !value || value.includes(',') ? 'all' : value; return !value || value.includes(',') ? 'all' : value;
@@ -312,12 +324,12 @@
<div class="mb-6 flex flex-wrap items-center justify-between gap-4"> <div class="mb-6 flex flex-wrap items-center justify-between gap-4">
<div> <div>
<h1 class="text-2xl font-semibold tracking-tight">Live</h1> <h1 class="text-2xl font-semibold tracking-tight">Поток</h1>
<p class="text-sm text-muted-foreground">SSE поток инцидентов и статуса флота.</p> <p class="text-sm text-muted-foreground">SSE поток инцидентов и статуса флота.</p>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Badge variant={connected ? 'default' : 'secondary'}> <Badge variant={connected ? 'default' : 'secondary'}>
{connected ? 'connected' : 'disconnected'} {connected ? 'подключено' : 'отключено'}
</Badge> </Badge>
<FleetLiveStaleBadge <FleetLiveStaleBadge
{isStale} {isStale}
@@ -329,7 +341,7 @@
/> />
<Button variant="outline" size="sm" onclick={() => connectStream()}> <Button variant="outline" size="sm" onclick={() => connectStream()}>
<RefreshCwIcon class="mr-1 size-4" /> <RefreshCwIcon class="mr-1 size-4" />
Reconnect Переподключить
</Button> </Button>
</div> </div>
</div> </div>
@@ -340,11 +352,11 @@
{#if err} {#if err}
<Alert class="mb-4"> <Alert class="mb-4">
<AlertTitle>Live stream</AlertTitle> <AlertTitle>Поток событий</AlertTitle>
<AlertDescription> <AlertDescription>
{err} {err}
{#if reconnectInSec != null} {#if reconnectInSec != null}
Переподключение через {reconnectInSec}s. Переподключение через {reconnectInSec} с.
{/if} {/if}
</AlertDescription> </AlertDescription>
</Alert> </Alert>
@@ -355,19 +367,17 @@
{#snippet icon()} {#snippet icon()}
<ActivityIcon class="size-5" aria-hidden="true" /> <ActivityIcon class="size-5" aria-hidden="true" />
{/snippet} {/snippet}
<Badge variant={statusVariant(snapshot?.status)}> <Badge variant={statusVariant(snapshot?.status)}>{statusLabel(snapshot?.status)}</Badge>
{snapshot?.status ?? 'healthy'}
</Badge>
</KpiStatCard> </KpiStatCard>
<KpiStatCard accent="warning" label="Partial" footer="неполный снимок"> <KpiStatCard accent="warning" label="Частичность" footer="неполный снимок">
{#snippet icon()} {#snippet icon()}
<LayersIcon class="size-5" aria-hidden="true" /> <LayersIcon class="size-5" aria-hidden="true" />
{/snippet} {/snippet}
{snapshot?.partial ? 'yes' : 'no'} {snapshot?.partial ? 'да' : 'нет'}
</KpiStatCard> </KpiStatCard>
<KpiStatCard <KpiStatCard
accent="info" accent="info"
label="Timestamp" label="Временная метка"
footer="время snapshot" footer="время snapshot"
valueClass="text-base font-normal tabular-nums leading-tight" valueClass="text-base font-normal tabular-nums leading-tight"
> >
@@ -379,7 +389,7 @@
</div> </div>
<DataTableCard <DataTableCard
title="Последние incidents из snapshot" title="Последние инциденты из снимка"
description="Всего: {snapshot?.counts?.total ?? snapshot?.incidents?.length ?? 0}" description="Всего: {snapshot?.counts?.total ?? snapshot?.incidents?.length ?? 0}"
> >
{#snippet toolbar()} {#snippet toolbar()}
@@ -388,7 +398,7 @@
<SearchIcon <SearchIcon
class="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 text-muted-foreground" class="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 text-muted-foreground"
/> />
<Input class="h-8 ps-8" placeholder="Поиск по severity, title, summary…" bind:value={liveIncSearch} /> <Input class="h-8 ps-8" placeholder="Поиск по критичности, заголовку, описанию…" bind:value={liveIncSearch} />
</div> </div>
<Button <Button
variant="outline" variant="outline"
@@ -410,13 +420,13 @@
active={liveSortKey === 'sev'} active={liveSortKey === 'sev'}
dir={liveSortDir} dir={liveSortDir}
onToggle={() => toggleLiveSort('sev')} onToggle={() => toggleLiveSort('sev')}
>Severity</SortableTh> >Критичность</SortableTh>
<SortableTh <SortableTh
active={liveSortKey === 'title'} active={liveSortKey === 'title'}
dir={liveSortDir} dir={liveSortDir}
onToggle={() => toggleLiveSort('title')} onToggle={() => toggleLiveSort('title')}
>Title</SortableTh> >Заголовок</SortableTh>
<Table.Head>Summary</Table.Head> <Table.Head>Описание</Table.Head>
<Table.Head> <Table.Head>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
Серверы Серверы
@@ -452,7 +462,7 @@
{#each filteredLiveIncidents as item (item.id)} {#each filteredLiveIncidents as item (item.id)}
<Table.Row> <Table.Row>
<Table.Cell> <Table.Cell>
<Badge variant={severityVariant(item.severity)}>{item.severity}</Badge> <Badge variant={severityVariant(item.severity)}>{severityLabel(item.severity)}</Badge>
</Table.Cell> </Table.Cell>
<Table.Cell class="font-medium">{item.title}</Table.Cell> <Table.Cell class="font-medium">{item.title}</Table.Cell>
<Table.Cell class="max-w-[380px] text-sm text-muted-foreground">{item.summary}</Table.Cell> <Table.Cell class="max-w-[380px] text-sm text-muted-foreground">{item.summary}</Table.Cell>
+2 -2
View File
@@ -136,7 +136,7 @@
</KpiStatCard> </KpiStatCard>
<KpiStatCard <KpiStatCard
accent="danger" accent="danger"
label="Bad connections" label="Плохие соединения"
valueClass="text-xl" valueClass="text-xl"
> >
{#snippet icon()} {#snippet icon()}
@@ -146,7 +146,7 @@
</KpiStatCard> </KpiStatCard>
<KpiStatCard <KpiStatCard
accent="warning" accent="warning"
label="Handshake timeouts" label="Таймауты рукопожатия"
valueClass="text-xl" valueClass="text-xl"
> >
{#snippet icon()} {#snippet icon()}
@@ -8,7 +8,7 @@
</script> </script>
<ServerPageHeader <ServerPageHeader
title="Configuration" title="Конфигурация"
subtitle="Конфигурация этой ноды не отдается через Control API" subtitle="Конфигурация этой ноды не отдается через Control API"
showRefresh={false} showRefresh={false}
/> />
@@ -57,22 +57,22 @@
} }
</script> </script>
<ServerPageHeader title="Runtime" {loading} onRefresh={load} /> <ServerPageHeader title="Состояние" {loading} onRefresh={load} />
<ServerQueryState {loading} {err} isEmpty={false}> <ServerQueryState {loading} {err} isEmpty={false}>
{#snippet children()} {#snippet children()}
<div class="mb-4 flex flex-wrap gap-2"> <div class="mb-4 flex flex-wrap gap-2">
{#if gates} {#if gates}
<Badge variant="secondary" class="px-3 py-1">accepting: {onOff(gates.accepting_new_connections)}</Badge> <Badge variant="secondary" class="px-3 py-1">Приём новых: {onOff(gates.accepting_new_connections)}</Badge>
<Badge variant="secondary" class="px-3 py-1">conditional cast: {onOff(gates.conditional_cast_enabled)}</Badge> <Badge variant="secondary" class="px-3 py-1">Условный cast: {onOff(gates.conditional_cast_enabled)}</Badge>
<Badge variant="secondary" class="px-3 py-1">me2dc fallback: {onOff(gates.me2dc_fallback_enabled)}</Badge> <Badge variant="secondary" class="px-3 py-1">Резерв me2dc: {onOff(gates.me2dc_fallback_enabled)}</Badge>
<Badge variant="secondary" class="px-3 py-1">ME ready: {onOff(gates.me_runtime_ready)}</Badge> <Badge variant="secondary" class="px-3 py-1">ME готов: {onOff(gates.me_runtime_ready)}</Badge>
<Badge variant="outline" class="px-3 py-1">middle proxy: {onOff(gates.use_middle_proxy)}</Badge> <Badge variant="outline" class="px-3 py-1">Промежуточный прокси: {onOff(gates.use_middle_proxy)}</Badge>
{/if} {/if}
</div> </div>
{#if dcs && dcs.middle_proxy_enabled && dcs.dcs} {#if dcs && dcs.middle_proxy_enabled && dcs.dcs}
<KpiPanelCard class="mb-6" accent="info" title="Datacenter status" contentClass="p-0"> <KpiPanelCard class="mb-6" accent="info" title="Статус дата-центров" contentClass="p-0">
{#snippet icon()} {#snippet icon()}
<BuildingIcon class="size-5" aria-hidden="true" /> <BuildingIcon class="size-5" aria-hidden="true" />
{/snippet} {/snippet}
@@ -81,8 +81,8 @@
<Table.Row class="border-b border-border/80 hover:bg-transparent"> <Table.Row class="border-b border-border/80 hover:bg-transparent">
<Table.Head>DC</Table.Head> <Table.Head>DC</Table.Head>
<Table.Head class="text-right">RTT</Table.Head> <Table.Head class="text-right">RTT</Table.Head>
<Table.Head class="text-right">Writers</Table.Head> <Table.Head class="text-right">Писатели</Table.Head>
<Table.Head class="text-right">Coverage</Table.Head> <Table.Head class="text-right">Покрытие</Table.Head>
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
<Table.Body> <Table.Body>
@@ -103,7 +103,7 @@
</KpiPanelCard> </KpiPanelCard>
{:else if dcs?.reason} {:else if dcs?.reason}
<Alert class="mb-4"> <Alert class="mb-4">
<AlertDescription>DC status: {dcs.reason}</AlertDescription> <AlertDescription>Статус ДЦ: {dcs.reason}</AlertDescription>
</Alert> </Alert>
{/if} {/if}
@@ -38,12 +38,12 @@
} }
</script> </script>
<ServerPageHeader title="Security" {loading} onRefresh={load} /> <ServerPageHeader title="Безопасность" {loading} onRefresh={load} />
<ServerQueryState {loading} {err} isEmpty={!data} emptyDescription="Endpoint вернул пустой ответ."> <ServerQueryState {loading} {err} isEmpty={!data} emptyDescription="Endpoint вернул пустой ответ.">
{#snippet children()} {#snippet children()}
{#if data} {#if data}
<ServerJsonCard accent="danger" title="Security posture" description="POST /security/posture" payload={data}> <ServerJsonCard accent="danger" title="Профиль безопасности" description="POST /security/posture" payload={data}>
{#snippet iconSnippet()} {#snippet iconSnippet()}
<ShieldIcon class="size-5" aria-hidden="true" /> <ShieldIcon class="size-5" aria-hidden="true" />
{/snippet} {/snippet}
@@ -39,7 +39,7 @@
} }
</script> </script>
<ServerPageHeader title="Update" {loading} onRefresh={load} /> <ServerPageHeader title="Обновление" {loading} onRefresh={load} />
<ServerQueryState {loading} {err} isEmpty={!sys} emptyDescription="Информация о бинарнике не получена."> <ServerQueryState {loading} {err} isEmpty={!sys} emptyDescription="Информация о бинарнике не получена.">
{#snippet children()} {#snippet children()}
@@ -38,14 +38,14 @@
} }
</script> </script>
<ServerPageHeader title="Upstreams &amp; DCs" {loading} onRefresh={load} /> <ServerPageHeader title="Апстримы и ДЦ" {loading} onRefresh={load} />
<ServerQueryState {loading} {err} isEmpty={!data} emptyDescription="Статистика upstream-ов пока не доступна."> <ServerQueryState {loading} {err} isEmpty={!data} emptyDescription="Статистика upstream-ов пока не доступна.">
{#snippet children()} {#snippet children()}
{#if data} {#if data}
<ServerJsonCard <ServerJsonCard
accent="info" accent="info"
title="Upstreams &amp; DCs" title="Апстримы и ДЦ"
description="stats/upstreams" description="stats/upstreams"
payload={data} payload={data}
preClass="max-h-[70vh]" preClass="max-h-[70vh]"
@@ -199,7 +199,7 @@
</script> </script>
<div class="mb-6 flex flex-wrap items-center justify-between gap-4"> <div class="mb-6 flex flex-wrap items-center justify-between gap-4">
<h1 class="text-2xl font-semibold tracking-tight">Users</h1> <h1 class="text-2xl font-semibold tracking-tight">Пользователи</h1>
<div class="flex gap-2"> <div class="flex gap-2">
<Button onclick={() => (createOpen = true)} disabled={readOnly}> <Button onclick={() => (createOpen = true)} disabled={readOnly}>
<PlusIcon class="mr-1 size-4" /> <PlusIcon class="mr-1 size-4" />
@@ -236,7 +236,7 @@
<Table.Row class="border-b border-border/80 hover:bg-transparent"> <Table.Row class="border-b border-border/80 hover:bg-transparent">
<Table.Head>Имя</Table.Head> <Table.Head>Имя</Table.Head>
<Table.Head>Ссылки</Table.Head> <Table.Head>Ссылки</Table.Head>
<Table.Head class="text-right">Conn</Table.Head> <Table.Head class="text-right">Соединения</Table.Head>
<Table.Head class="text-right">IP</Table.Head> <Table.Head class="text-right">IP</Table.Head>
<Table.Head class="text-right">Трафик</Table.Head> <Table.Head class="text-right">Трафик</Table.Head>
<Table.Head class="w-[100px]"></Table.Head> <Table.Head class="w-[100px]"></Table.Head>
@@ -248,9 +248,9 @@
<Table.Cell class="font-medium">{u.username}</Table.Cell> <Table.Cell class="font-medium">{u.username}</Table.Cell>
<Table.Cell> <Table.Cell>
<div class="flex flex-wrap gap-1"> <div class="flex flex-wrap gap-1">
{#each u.links?.tls ?? [] as link (link)} {#each u.links?.tls ?? [] as link, idx (`${link}:${idx}`)}
<Button variant="outline" size="xs" class="h-7" onclick={() => copy(link)}> <Button variant="outline" size="xs" class="h-7" onclick={() => copy(link)}>
TLS <CopyIcon class="size-3" /> TLS {idx + 1} <CopyIcon class="size-3" />
</Button> </Button>
{/each} {/each}
</div> </div>
@@ -269,6 +269,8 @@
size="icon-sm" size="icon-sm"
disabled={readOnly} disabled={readOnly}
onclick={() => openEdit(u)} onclick={() => openEdit(u)}
aria-label="Редактировать пользователя"
title="Редактировать пользователя"
> >
<PencilIcon class="size-4" /> <PencilIcon class="size-4" />
</Button> </Button>
@@ -277,6 +279,8 @@
size="icon-sm" size="icon-sm"
disabled={readOnly} disabled={readOnly}
onclick={() => (deleteUserRow = u)} onclick={() => (deleteUserRow = u)}
aria-label="Удалить пользователя"
title="Удалить пользователя"
> >
<TrashIcon class="size-4 text-destructive" /> <TrashIcon class="size-4 text-destructive" />
</Button> </Button>
@@ -325,15 +329,15 @@
<Input id="eq" bind:value={editQuota} type="number" min="0" /> <Input id="eq" bind:value={editQuota} type="number" min="0" />
</div> </div>
<div class="grid gap-2"> <div class="grid gap-2">
<Label for="em">max_unique_ips</Label> <Label for="em">Макс. уникальных IP (max_unique_ips)</Label>
<Input id="em" bind:value={editMaxIp} type="number" min="0" /> <Input id="em" bind:value={editMaxIp} type="number" min="0" />
</div> </div>
<div class="grid gap-2"> <div class="grid gap-2">
<Label for="et">max_tcp_conns</Label> <Label for="et">Макс. TCP-соединений (max_tcp_conns)</Label>
<Input id="et" bind:value={editMaxTcp} type="number" min="0" /> <Input id="et" bind:value={editMaxTcp} type="number" min="0" />
</div> </div>
<div class="grid gap-2"> <div class="grid gap-2">
<Label for="ex">expiry (RFC3339)</Label> <Label for="ex">Срок действия (RFC3339)</Label>
<Input id="ex" bind:value={editExp} class="font-mono text-sm" /> <Input id="ex" bind:value={editExp} class="font-mono text-sm" />
</div> </div>
{#if formErr} {#if formErr}