feat(runtime-logs): enhance runtime log management and configuration

Добавлены новые возможности для управления файловыми логами в Docker-сервисах:
- Обновлены конфигурации для поддержки логов, включая переменные окружения и монтирование директорий.
- Документация обновлена для описания новых эндпоинтов и параметров, связанных с логами.
- Упрощен доступ к логам через API и интерфейс пользователя.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-06-12 19:18:50 +07:00
co-authored by Cursor
parent 3f0dd6c234
commit 0c5502b5bb
26 changed files with 1188 additions and 294 deletions
@@ -0,0 +1,80 @@
<script lang="ts">
import { onMount } from 'svelte';
import { resolve } from '$app/paths';
import { loadSettings, partitionSettings } from '$lib/settings/settings-api.js';
import { BIRD_SETTING_KEYS } from '$lib/settings/settings-known-keys.js';
import { Button } from '$lib/ui/core/button/index.js';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '$lib/ui/core/card/index.js';
import { notifyApiError } from '$lib/ui/app/toast.js';
import ArrowRight from '@lucide/svelte/icons/arrow-right';
import SlidersHorizontal from '@lucide/svelte/icons/sliders-horizontal';
const labels: Record<string, string> = {
bird_router_id: 'Router ID',
bird_local_ipv4: 'Local IPv4',
bird_local_ipv6: 'Local IPv6',
bird_local_asn: 'Local ASN',
bird_bgp_source_ipv4: 'BGP source IPv4',
bird_bgp_source_ipv6: 'BGP source IPv6'
};
let loading = $state(true);
let values = $state<Record<string, string>>({});
onMount(() => {
void (async () => {
loading = true;
try {
const settings = await loadSettings();
const { partitioned } = partitionSettings(settings);
const out: Record<string, string> = {};
for (const key of BIRD_SETTING_KEYS) {
const v = String(partitioned.bird[key] ?? '').trim();
if (v) out[key] = v;
}
values = out;
} catch (e) {
notifyApiError(e);
} finally {
loading = false;
}
})();
});
</script>
<Card>
<CardHeader>
<CardTitle>BIRD (кратко)</CardTitle>
<CardDescription>
Глобальные параметры BIRD из tenant settings. Полная форма — в разделе «Параметры».
</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
{#if loading}
<p class="text-sm text-muted-foreground">Загрузка…</p>
{:else if Object.keys(values).length === 0}
<p class="text-sm text-muted-foreground">Параметры BIRD ещё не заданы.</p>
{:else}
<dl class="grid gap-2 text-sm sm:grid-cols-2">
{#each Object.entries(values) as [key, value] (key)}
<div class="rounded-md border bg-muted/30 px-3 py-2">
<dt class="text-muted-foreground">{labels[key] ?? key}</dt>
<dd class="font-mono text-xs break-all">{value}</dd>
</div>
{/each}
</dl>
{/if}
<Button variant="outline" href={resolve('/tenant-settings?tab=bird')}>
<SlidersHorizontal class="size-4" />
Изменить параметры
<ArrowRight class="size-4" />
</Button>
</CardContent>
</Card>
@@ -1,223 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { defaults, superForm } from 'sveltekit-superforms';
import { zod4 } from 'sveltekit-superforms/adapters';
import {
emptyRevisionSettingsForm,
revisionSettingsSchema
} from '$lib/settings/revision-settings.schema.js';
import {
buildPayloadFromFormFields,
loadSettings,
partitionSettings,
patchSettings,
type AdditionalSettingEntry
} from '$lib/settings/settings-api.js';
import { REVISION_SETTING_KEYS } from '$lib/settings/settings-known-keys.js';
import { Button } from '$lib/ui/core/button/index.js';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '$lib/ui/core/card/index.js';
import { Input } from '$lib/ui/core/input/index.js';
import { Alert, AlertDescription, AlertTitle } from '$lib/ui/core/alert/index.js';
import FormField from '$lib/ui/patterns/form/form-field.svelte';
import EmptyState from '$lib/ui/patterns/empty-state/empty-state.svelte';
import { notify, notifyApiError } from '$lib/ui/app/toast.js';
import Save from '@lucide/svelte/icons/save';
import Plus from '@lucide/svelte/icons/plus';
import Trash2 from '@lucide/svelte/icons/trash-2';
import Info from '@lucide/svelte/icons/info';
let loading = $state(false);
let saving = $state(false);
let loaded = $state(false);
let additionalSettings = $state<AdditionalSettingEntry[]>([]);
let additionalIdCounter = $state(1);
const { form, errors, reset, validateForm } = superForm(
defaults(emptyRevisionSettingsForm(), zod4(revisionSettingsSchema)),
{
validators: zod4(revisionSettingsSchema),
SPA: true,
dataType: 'json'
}
);
let hasValidationErrors = $derived(Boolean($errors.revision_retention_minutes?.length));
function addAdditionalSetting() {
additionalSettings.push({ id: additionalIdCounter++, key: '', value: '' });
}
function removeAdditionalSetting(id: number) {
additionalSettings = additionalSettings.filter((entry) => entry.id !== id);
}
let canSave = $derived.by(() => {
if (loading || saving || hasValidationErrors || !loaded) return false;
const hasRetention = String($form.revision_retention_minutes ?? '').trim() !== '';
const hasAdditional = additionalSettings.some((entry) => entry.key.trim() !== '');
return hasRetention || hasAdditional;
});
async function load() {
loading = true;
try {
const settings = await loadSettings();
const { partitioned, nextId } = partitionSettings(settings, additionalIdCounter);
reset({ data: partitioned.revision });
additionalSettings = partitioned.additional;
additionalIdCounter = nextId;
loaded = true;
} catch (e) {
notifyApiError(e);
} finally {
loading = false;
}
}
async function save() {
const validation = await validateForm({ update: true });
if (!validation.valid) {
notify.error('Исправьте ошибки в полях настроек');
return;
}
if (!canSave) {
notify.error('Нечего сохранять или есть ошибки в полях');
return;
}
const payload = buildPayloadFromFormFields(
REVISION_SETTING_KEYS,
$form as Record<string, string>,
$errors as Partial<Record<string, string[]>>
);
for (const entry of additionalSettings) {
const key = entry.key.trim();
if (!key) continue;
payload[key] = entry.value;
}
saving = true;
try {
await patchSettings(payload);
notify.success('Системные настройки сохранены');
await load();
} catch (e) {
notifyApiError(e);
} finally {
saving = false;
}
}
onMount(() => {
void load();
});
</script>
<div class="flex flex-col gap-6">
<Alert class="border-info/30 bg-info/5">
<Info class="text-info" />
<AlertTitle>Operator-only</AlertTitle>
<AlertDescription>
Изменение параметров через <code class="text-xs">PATCH /v1/settings</code> требует роли operator.
При отсутствии прав API вернёт 403.
</AlertDescription>
</Alert>
<Card>
<CardHeader>
<CardTitle>Хранение ревизий</CardTitle>
<CardDescription>
Автоматическая очистка старых ревизий. Последняя раскатанная ревизия не удаляется.
</CardDescription>
</CardHeader>
<CardContent>
{#if loading && !loaded}
<p class="text-sm text-muted-foreground">Загрузка…</p>
{:else if !loaded}
<Button variant="outline" onclick={load}>Загрузить настройки</Button>
{:else}
<FormField
id="revision-retention-minutes"
label="Время жизни ревизий, мин (revision_retention_minutes)"
error={$errors.revision_retention_minutes?.[0]}
description="Допустимый диапазон: 1543200 минут."
>
<Input
id="revision-retention-minutes"
type="number"
min="15"
max="43200"
bind:value={$form.revision_retention_minutes}
placeholder="43200"
/>
</FormField>
{/if}
</CardContent>
</Card>
<Card>
<CardHeader>
<div class="flex items-center justify-between gap-4">
<div class="space-y-1">
<CardTitle>Дополнительные параметры</CardTitle>
<CardDescription>Произвольные KV-пары в global_settings.</CardDescription>
</div>
{#if loaded}
<Button variant="outline" size="sm" onclick={addAdditionalSetting}>
<Plus class="size-4" />
Добавить строку
</Button>
{/if}
</div>
</CardHeader>
<CardContent>
{#if !loaded}
<p class="text-sm text-muted-foreground">Загрузите настройки выше.</p>
{:else if additionalSettings.length === 0}
<EmptyState
title="Нет дополнительных параметров"
description="Добавьте KV-пару при необходимости."
/>
{:else}
<div class="space-y-2">
{#each additionalSettings as entry (entry.id)}
<div class="grid grid-cols-1 gap-2 md:grid-cols-[1fr_1fr_auto]">
<Input bind:value={entry.key} placeholder="Ключ (например, bird_log_level)" />
<Input bind:value={entry.value} placeholder="Значение (строка)" />
<Button
variant="ghost"
size="icon"
aria-label="Удалить строку"
onclick={() => removeAdditionalSetting(entry.id)}
>
<Trash2 class="size-4" />
</Button>
</div>
{/each}
</div>
{/if}
</CardContent>
</Card>
{#if loaded}
{#if hasValidationErrors}
<p class="text-sm text-destructive">
Есть ошибки в полях. Исправьте их, чтобы сохранить изменения.
</p>
{/if}
<Button onclick={save} disabled={!canSave}>
<Save />
{saving ? 'Сохранение…' : 'Применить настройки'}
</Button>
{/if}
</div>
@@ -0,0 +1,149 @@
<script lang="ts">
import { onMount } from 'svelte';
import {
loadSettings,
partitionSettings,
patchSettings,
type AdditionalSettingEntry
} from '$lib/settings/settings-api.js';
import { Button } from '$lib/ui/core/button/index.js';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '$lib/ui/core/card/index.js';
import { Input } from '$lib/ui/core/input/index.js';
import EmptyState from '$lib/ui/patterns/empty-state/empty-state.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js';
import Save from '@lucide/svelte/icons/save';
import Plus from '@lucide/svelte/icons/plus';
import Trash2 from '@lucide/svelte/icons/trash-2';
let loading = $state(false);
let saving = $state(false);
let loaded = $state(false);
let additionalSettings = $state<AdditionalSettingEntry[]>([]);
let additionalIdCounter = $state(1);
let canSave = $derived.by(() => {
if (loading || saving || !loaded) return false;
return additionalSettings.some((entry) => entry.key.trim() !== '');
});
function addAdditionalSetting() {
additionalSettings.push({ id: additionalIdCounter++, key: '', value: '' });
}
function requestRemoveAdditionalSetting(entry: AdditionalSettingEntry) {
const key = entry.key.trim();
void confirm({
title: key ? `Удалить параметр «${key}»?` : 'Удалить строку?',
description: key
? 'Строка исчезнет из формы. Чтобы удалить ключ из tenant, сохраните без него или очистите значение и примените PATCH.'
: 'Несохранённая пустая строка будет удалена из формы.',
confirmLabel: 'Удалить',
destructive: Boolean(key),
onConfirm: async () => {
additionalSettings = additionalSettings.filter((row) => row.id !== entry.id);
}
});
}
async function load() {
loading = true;
try {
const settings = await loadSettings();
const { partitioned, nextId } = partitionSettings(settings, additionalIdCounter);
additionalSettings = partitioned.additional;
additionalIdCounter = nextId;
loaded = true;
} catch (e) {
notifyApiError(e);
} finally {
loading = false;
}
}
async function save() {
if (!canSave) {
notify.error('Нечего сохранять');
return;
}
const payload: Record<string, string> = {};
for (const entry of additionalSettings) {
const key = entry.key.trim();
if (!key) continue;
payload[key] = entry.value;
}
saving = true;
try {
await patchSettings(payload);
notify.success('Дополнительные параметры сохранены');
await load();
} catch (e) {
notifyApiError(e);
} finally {
saving = false;
}
}
onMount(() => {
void load();
});
</script>
<Card>
<CardHeader>
<div class="flex items-center justify-between gap-4">
<div class="space-y-1">
<CardTitle>Дополнительные параметры</CardTitle>
<CardDescription>Произвольные KV-пары в global_settings (operator).</CardDescription>
</div>
{#if loaded}
<Button variant="outline" size="sm" onclick={addAdditionalSetting}>
<Plus class="size-4" />
Добавить строку
</Button>
{/if}
</div>
</CardHeader>
<CardContent class="space-y-4">
{#if loading && !loaded}
<p class="text-sm text-muted-foreground">Загрузка…</p>
{:else if !loaded}
<Button variant="outline" onclick={load}>Загрузить настройки</Button>
{:else if additionalSettings.length === 0}
<EmptyState
title="Нет дополнительных параметров"
description="Добавьте KV-пару при необходимости."
/>
{:else}
<div class="space-y-2">
{#each additionalSettings as entry (entry.id)}
<div class="grid grid-cols-1 gap-2 md:grid-cols-[1fr_1fr_auto]">
<Input bind:value={entry.key} placeholder="Ключ (например, bird_log_level)" />
<Input bind:value={entry.value} placeholder="Значение (строка)" />
<Button
variant="ghost"
size="icon"
aria-label="Удалить строку"
onclick={() => requestRemoveAdditionalSetting(entry)}
>
<Trash2 class="size-4" />
</Button>
</div>
{/each}
</div>
<Button onclick={save} disabled={!canSave}>
<Save />
{saving ? 'Сохранение…' : 'Применить дополнительные параметры'}
</Button>
{/if}
</CardContent>
</Card>
@@ -104,7 +104,7 @@
<Card>
<CardHeader>
<CardTitle>Control plane</CardTitle>
<CardTitle>BIRD control plane</CardTitle>
<CardDescription>
Глобальные параметры BIRD для pipeline refresh/apply. Сохранение через
<code class="text-xs">PATCH /v1/settings</code> (роль operator).
@@ -115,8 +115,8 @@
<Info class="text-info" />
<AlertTitle>Подстановка в конфиг</AlertTitle>
<AlertDescription>
Значения используются при генерации BIRD-конфигурации в pipeline (router id, local AS,
адреса). Пиры и спикеры настраиваются на соседних вкладках.
Значения используются при генерации BIRD-конфигурации (router id, local AS, адреса). Пиры и
спикеры настраиваются в разделе «Сеть».
</AlertDescription>
</Alert>
@@ -205,7 +205,7 @@
<Button onclick={save} disabled={!canSave}>
<Save />
{saving ? 'Сохранение…' : 'Применить параметры'}
{saving ? 'Сохранение…' : 'Применить параметры BIRD'}
</Button>
{/if}
</CardContent>
@@ -0,0 +1,138 @@
<script lang="ts">
import { onMount } from 'svelte';
import { defaults, superForm } from 'sveltekit-superforms';
import { zod4 } from 'sveltekit-superforms/adapters';
import {
emptyRevisionSettingsForm,
revisionSettingsSchema
} from '$lib/settings/revision-settings.schema.js';
import {
buildPayloadFromFormFields,
loadSettings,
partitionSettings,
patchSettings
} from '$lib/settings/settings-api.js';
import { REVISION_SETTING_KEYS } from '$lib/settings/settings-known-keys.js';
import { Button } from '$lib/ui/core/button/index.js';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '$lib/ui/core/card/index.js';
import { Input } from '$lib/ui/core/input/index.js';
import FormField from '$lib/ui/patterns/form/form-field.svelte';
import { notify, notifyApiError } from '$lib/ui/app/toast.js';
import Save from '@lucide/svelte/icons/save';
let loading = $state(false);
let saving = $state(false);
let loaded = $state(false);
const { form, errors, reset, validateForm } = superForm(
defaults(emptyRevisionSettingsForm(), zod4(revisionSettingsSchema)),
{
validators: zod4(revisionSettingsSchema),
SPA: true,
dataType: 'json'
}
);
let hasValidationErrors = $derived(Boolean($errors.revision_retention_minutes?.length));
let canSave = $derived.by(() => {
if (loading || saving || hasValidationErrors || !loaded) return false;
return String($form.revision_retention_minutes ?? '').trim() !== '';
});
async function load() {
loading = true;
try {
const settings = await loadSettings();
const { partitioned } = partitionSettings(settings);
reset({ data: partitioned.revision });
loaded = true;
} catch (e) {
notifyApiError(e);
} finally {
loading = false;
}
}
async function save() {
const validation = await validateForm({ update: true });
if (!validation.valid) {
notify.error('Исправьте ошибки в полях настроек');
return;
}
if (!canSave) {
notify.error('Нечего сохранять или есть ошибки в полях');
return;
}
const payload = buildPayloadFromFormFields(
REVISION_SETTING_KEYS,
$form as Record<string, string>,
$errors as Partial<Record<string, string[]>>
);
saving = true;
try {
await patchSettings(payload);
notify.success('Параметры хранения ревизий сохранены');
await load();
} catch (e) {
notifyApiError(e);
} finally {
saving = false;
}
}
onMount(() => {
void load();
});
</script>
<Card>
<CardHeader>
<CardTitle>Хранение ревизий</CardTitle>
<CardDescription>
Автоматическая очистка старых ревизий. Последняя раскатанная ревизия не удаляется.
</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
{#if loading && !loaded}
<p class="text-sm text-muted-foreground">Загрузка…</p>
{:else if !loaded}
<Button variant="outline" onclick={load}>Загрузить настройки</Button>
{:else}
<FormField
id="revision-retention-minutes"
label="Время жизни ревизий, мин (revision_retention_minutes)"
error={$errors.revision_retention_minutes?.[0]}
description="Допустимый диапазон: 1543200 минут."
>
<Input
id="revision-retention-minutes"
type="number"
min="15"
max="43200"
bind:value={$form.revision_retention_minutes}
placeholder="43200"
/>
</FormField>
{#if hasValidationErrors}
<p class="text-sm text-destructive">
Есть ошибки в полях. Исправьте их, чтобы сохранить изменения.
</p>
{/if}
<Button onclick={save} disabled={!canSave}>
<Save />
{saving ? 'Сохранение…' : 'Применить параметры ревизий'}
</Button>
{/if}
</CardContent>
</Card>
@@ -0,0 +1,84 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/state';
import PageHeader from '$lib/ui/app/page-header/page-header.svelte';
import { Alert, AlertDescription, AlertTitle } from '$lib/ui/core/alert/index.js';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '$lib/ui/core/tabs/index.js';
import TenantBirdSettingsCard from '$lib/components/tenant-settings/TenantBirdSettingsCard.svelte';
import TenantRevisionSettingsCard from '$lib/components/tenant-settings/TenantRevisionSettingsCard.svelte';
import TenantAdditionalSettingsCard from '$lib/components/tenant-settings/TenantAdditionalSettingsCard.svelte';
import SlidersHorizontal from '@lucide/svelte/icons/sliders-horizontal';
import Info from '@lucide/svelte/icons/info';
type TenantSettingsTab = 'bird' | 'revision' | 'additional';
function parseTenantSettingsTab(value: string | null): TenantSettingsTab {
if (value === 'revision' || value === 'additional') return value;
return 'bird';
}
let activeTab = $state<TenantSettingsTab>('bird');
let tabSyncReady = $state(false);
onMount(() => {
activeTab = parseTenantSettingsTab(page.url.searchParams.get('tab'));
tabSyncReady = true;
});
function syncTabToUrl(tab: TenantSettingsTab) {
if (!tabSyncReady) return;
const url = new URL(page.url);
if (tab === 'bird') url.searchParams.delete('tab');
else url.searchParams.set('tab', tab);
const next = `${url.pathname}${url.search}${url.hash}`;
if (next !== `${page.url.pathname}${page.url.search}${page.url.hash}`) {
void goto(next, { replaceState: true, keepFocus: true, noScroll: true });
}
}
$effect(() => {
if (!tabSyncReady) return;
syncTabToUrl(activeTab);
});
</script>
<div class="flex flex-col gap-6">
<PageHeader
title="Параметры tenant"
description="Параметры control plane для текущего tenant (API /v1/settings). Токен и тема интерфейса — в разделе «Настройки»."
icon={SlidersHorizontal}
iconClass="bg-chart-5/15 text-chart-5"
/>
<Alert class="border-info/30 bg-info/5">
<Info class="text-info" />
<AlertTitle>Operator-only</AlertTitle>
<AlertDescription>
Изменение значений через <code class="text-xs">PATCH /v1/settings</code> требует роли operator.
При отсутствии прав API вернёт 403.
</AlertDescription>
</Alert>
<Tabs bind:value={activeTab}>
<div class="overflow-x-auto pb-1 [scrollbar-gutter:stable]">
<TabsList class="inline-flex min-w-max">
<TabsTrigger value="bird">BIRD</TabsTrigger>
<TabsTrigger value="revision">Ревизии</TabsTrigger>
<TabsTrigger value="additional">Дополнительно</TabsTrigger>
</TabsList>
</div>
<TabsContent value="bird" class="mt-4">
<TenantBirdSettingsCard />
</TabsContent>
<TabsContent value="revision" class="mt-4">
<TenantRevisionSettingsCard />
</TabsContent>
<TabsContent value="additional" class="mt-4">
<TenantAdditionalSettingsCard />
</TabsContent>
</Tabs>
</div>
@@ -5,7 +5,7 @@ import { emptyRevisionSettingsForm, revisionSettingsSchema } from './revision-se
/** @deprecated Используйте birdSettingsSchema и revisionSettingsSchema отдельно. */
export const settingsKnownSchema = birdSettingsSchema.merge(revisionSettingsSchema);
/** @deprecated Используйте BirdSettingsForm и RevisionSettingsForm. */
/** @deprecated Используйте TenantBirdSettingsCard и TenantRevisionSettingsCard. */
export type SettingsKnownForm = z.infer<typeof settingsKnownSchema>;
/** @deprecated Используйте emptyBirdSettingsForm и emptyRevisionSettingsForm. */
+3 -1
View File
@@ -8,6 +8,7 @@ import LayoutDashboard from '@lucide/svelte/icons/layout-dashboard';
import Network from '@lucide/svelte/icons/network';
import Settings from '@lucide/svelte/icons/settings';
import Shield from '@lucide/svelte/icons/shield';
import SlidersHorizontal from '@lucide/svelte/icons/sliders-horizontal';
export type NavItem = {
href: string;
label: string;
@@ -21,7 +22,8 @@ export const mainNav: NavItem[] = [
{ href: '/network', label: 'Сеть', icon: Network },
{ href: '/operations', label: 'Ревизии', icon: Activity },
{ href: '/schedule', label: 'Расписание', icon: CalendarClock },
{ href: '/monitoring', label: 'Мониторинг', icon: Gauge }
{ href: '/monitoring', label: 'Мониторинг', icon: Gauge },
{ href: '/tenant-settings', label: 'Параметры', icon: SlidersHorizontal }
];
export const bottomNav: NavItem[] = [
+2 -2
View File
@@ -19,7 +19,7 @@
import { notifyApiError } from '$lib/ui/app/toast.js';
import NetworkPeersCard from '$lib/components/network/NetworkPeersCard.svelte';
import NetworkSpeakersCard from '$lib/components/network/NetworkSpeakersCard.svelte';
import BirdSettingsForm from '$lib/components/network/BirdSettingsForm.svelte';
import NetworkBirdSettingsSummaryCard from '$lib/components/network/NetworkBirdSettingsSummaryCard.svelte';
import NetworkOverviewTab from '$lib/components/network/NetworkOverviewTab.svelte';
import NetworkSpeakerDetailSheet from '$lib/components/network/NetworkSpeakerDetailSheet.svelte';
import NetworkAutoRefreshToggle from '$lib/components/network/NetworkAutoRefreshToggle.svelte';
@@ -238,7 +238,7 @@
</TabsContent>
<TabsContent value="control-plane" class="mt-4">
<BirdSettingsForm />
<NetworkBirdSettingsSummaryCard />
</TabsContent>
</Tabs>
</div>
+12 -16
View File
@@ -53,7 +53,6 @@
import OperationsDiffTab from '$lib/components/operations/OperationsDiffTab.svelte';
import OperationsJobsTab from '$lib/components/operations/OperationsJobsTab.svelte';
import OperationsJobsFilters from '$lib/components/operations/OperationsJobsFilters.svelte';
import OperationsSystemSettingsTab from '$lib/components/operations/OperationsSystemSettingsTab.svelte';
import type {
JobDetailedReport,
JobLogEntry,
@@ -82,10 +81,10 @@
import Info from '@lucide/svelte/icons/info';
import ArrowRight from '@lucide/svelte/icons/arrow-right';
type OpsTab = 'revisions' | 'diff' | 'jobs' | 'system';
type OpsTab = 'revisions' | 'diff' | 'jobs';
function parseOpsTab(value: string | null): OpsTab {
if (value === 'diff' || value === 'jobs' || value === 'system') return value;
if (value === 'diff' || value === 'jobs') return value;
return 'revisions';
}
@@ -351,7 +350,12 @@
}
onMount(() => {
activeTab = parseOpsTab(page.url.searchParams.get('tab'));
const tabParam = page.url.searchParams.get('tab');
if (tabParam === 'system') {
void goto(resolve('/tenant-settings?tab=revision'), { replaceState: true });
return;
}
activeTab = parseOpsTab(tabParam);
lastLoadedTab = activeTab;
tabSyncReady = true;
void refreshActiveTab(true);
@@ -459,9 +463,6 @@
break;
case 'diff':
break;
case 'system':
await loadBirdStatus();
break;
default:
await loadRevisions();
}
@@ -928,12 +929,12 @@
<Alert class="border-info/30 bg-info/5">
<Info class="text-info" />
<AlertTitle>Четыре раздела на одной странице</AlertTitle>
<AlertTitle>Три раздела на одной странице</AlertTitle>
<AlertDescription>
<strong>Ревизии</strong> — история конфигов и откат; <strong>Сравнение</strong> — diff
префиксов;
<strong>Задачи</strong> — ingest, apply, rollback; <strong>Система</strong> — TTL ревизий и
дополнительные KV. Apply и Reload требуют operator. Сводный мониторинг BGP — на
префиксов; <strong>Задачи</strong> — ingest, apply, rollback. TTL ревизий и tenant KV — в
<Button variant="link" class="h-auto p-0" href={resolve('/tenant-settings')}>Параметры</Button
>. Apply и Reload требуют operator. Сводный мониторинг BGP — на
<Button variant="link" class="h-auto p-0" href={resolve('/monitoring')}>Мониторинг</Button>.
</AlertDescription>
</Alert>
@@ -964,7 +965,6 @@
<TabsTrigger value="revisions">Ревизии ({revisions.length})</TabsTrigger>
<TabsTrigger value="diff">Сравнение</TabsTrigger>
<TabsTrigger value="jobs">Задачи ({jobs.length})</TabsTrigger>
<TabsTrigger value="system">Система</TabsTrigger>
</TabsList>
</div>
@@ -1029,10 +1029,6 @@
jobStatusVariant={jobStatusBadgeVariant}
/>
</TabsContent>
<TabsContent value="system" class="mt-4">
<OperationsSystemSettingsTab />
</TabsContent>
</Tabs>
</div>
@@ -0,0 +1,5 @@
<script lang="ts">
import TenantSettingsPage from '$lib/components/tenant-settings/TenantSettingsPage.svelte';
</script>
<TenantSettingsPage />