From 5fca165c6986465f671d5a9a25786ba6cc60f134 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 21 May 2026 10:11:04 +0700 Subject: [PATCH] refactor(settings): deprecate settingsKnownSchema and integrate bird and revision settings - Merged birdSettingsSchema and revisionSettingsSchema into settingsKnownSchema, marking the previous schema as deprecated. - Updated emptySettingsKnownForm to utilize emptyBirdSettingsForm and emptyRevisionSettingsForm. - Refactored layout and page components to streamline theme management and improve tab synchronization in network and operations pages. - Introduced new system settings tab in operations and updated settings page to manage theme preferences. --- .cursor/commands/commit-message.md | 2 +- .cursor/rules/conventional-commits.mdc | 6 +- .cursor/skills/commit-message/SKILL.md | 2 +- .releaserc.json | 1 + docs/releasing.md | 6 +- .../network/BirdSettingsForm.svelte | 212 +++++++++ .../OperationsSystemSettingsTab.svelte | 223 +++++++++ web/src/lib/settings/bird-settings.schema.ts | 24 + web/src/lib/settings/ip-validation.ts | 47 ++ .../lib/settings/revision-settings.schema.ts | 19 + web/src/lib/settings/settings-api.ts | 89 ++++ web/src/lib/settings/settings-known-keys.ts | 21 + web/src/lib/settings/settings-known.schema.ts | 80 +--- web/src/lib/theme-preferences.svelte.ts | 28 ++ web/src/routes/+layout.svelte | 21 +- web/src/routes/network/+page.svelte | 52 ++- web/src/routes/operations/+page.svelte | 16 +- web/src/routes/settings/+page.svelte | 433 +++--------------- 18 files changed, 807 insertions(+), 475 deletions(-) create mode 100644 web/src/lib/components/network/BirdSettingsForm.svelte create mode 100644 web/src/lib/components/operations/OperationsSystemSettingsTab.svelte create mode 100644 web/src/lib/settings/bird-settings.schema.ts create mode 100644 web/src/lib/settings/ip-validation.ts create mode 100644 web/src/lib/settings/revision-settings.schema.ts create mode 100644 web/src/lib/settings/settings-api.ts create mode 100644 web/src/lib/settings/settings-known-keys.ts create mode 100644 web/src/lib/theme-preferences.svelte.ts diff --git a/.cursor/commands/commit-message.md b/.cursor/commands/commit-message.md index 35a84ff..af4744d 100644 --- a/.cursor/commands/commit-message.md +++ b/.cursor/commands/commit-message.md @@ -32,6 +32,6 @@ powershell -NoProfile -File scripts/commit/staged-context.ps1 - Новая пользовательская возможность → `feat` (minor) - Починка ожидаемого поведения / баг → `fix` (patch) - Follow-up баги после недавнего `feat` в том же scope → **`fix`**, не `feat` -- Только перестройка без нового поведения → `refactor` (none) +- Только перестройка без нового поведения → `refactor` (patch, без новых функций) Заголовок — EN, императив, ≤72 символов. Тело — RU. diff --git a/.cursor/rules/conventional-commits.mdc b/.cursor/rules/conventional-commits.mdc index b008b5f..1b88fc9 100644 --- a/.cursor/rules/conventional-commits.mdc +++ b/.cursor/rules/conventional-commits.mdc @@ -50,7 +50,7 @@ powershell -NoProfile -File scripts/commit/staged-context.ps1 | `feat` | **новая** пользовательская возможность (раньше нельзя было) | minor | | `fix` | восстановление **ожидаемого** поведения; баг, регрессия, падение UI | patch | | `perf` | ускорение без смены API | patch | -| `refactor` | реструктуризация **без** новой возможности и **без** исправления бага | — | +| `refactor` | реструктуризация **без** новой возможности и **без** исправления бага | patch | | `docs` | только документация | — | | `test` | тесты | — | | `ci` | CI/CD (`.gitea/`, workflows); правки, из‑за которых нужны новые образы | patch | @@ -62,7 +62,7 @@ powershell -NoProfile -File scripts/commit/staged-context.ps1 1. Появилось **новое** действие / экран / API / настройка, которых не было → `feat` 2. То, что **должно было работать**, не работало (кнопки, диалоги, сохранение, 500) → `fix` -3. Только перестройка кода или UI на другой паттерн, поведение для пользователя то же → `refactor` +3. Только перестройка кода или UI на другой паттерн, поведение для пользователя то же → `refactor` (patch, без новых функций) 4. Ускорение без изменения контракта → `perf` **Не путать с формулировкой diff:** @@ -105,7 +105,7 @@ feat(web): migrate modules list to AppDataTable # Хорошо — если не было нового user-facing refactor(web): migrate modules list to AppDataTable -Единый паттерн таблиц; поведение списка модулей без изменений. +Единый паттерн таблиц; поведение списка модулей без изменений. Semver: patch. ``` ``` diff --git a/.cursor/skills/commit-message/SKILL.md b/.cursor/skills/commit-message/SKILL.md index 0168b88..5a38ebb 100644 --- a/.cursor/skills/commit-message/SKILL.md +++ b/.cursor/skills/commit-message/SKILL.md @@ -101,7 +101,7 @@ git commit -m "$( @' | Пользователь получает **новую** возможность? | `feat` (minor) | | Восстанавливается **ожидаемое** поведение / устранён баг? | `fix` (patch) | | Только скорость, контракт тот же? | `perf` (patch) | -| Только структура кода/UI, поведение то же? | `refactor` (none) | +| Только структура кода/UI, поведение то же? | `refactor` (patch) | **Follow-up:** правки сразу после `feat` в том же scope без новой возможности → **`fix`**, не `feat` (слова *enhance/improve/refactor* в задаче не делают commit `feat`). diff --git a/.releaserc.json b/.releaserc.json index 18bce23..350fcf9 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -11,6 +11,7 @@ { "type": "fix", "release": "patch" }, { "type": "perf", "release": "patch" }, { "type": "ci", "release": "patch" }, + { "type": "refactor", "release": "patch" }, { "breaking": true, "release": "major" } ] } diff --git a/docs/releasing.md b/docs/releasing.md index e0a8eab..6e8c367 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -7,9 +7,11 @@ EvoBGP использует [Conventional Commits](https://www.conventionalcommi | Тип коммита | Bump | |-------------|------| | `feat` | minor (1.0.0 → 1.1.0) | -| `fix`, `perf`, `ci` | patch (1.5.1 → 1.5.2) | +| `fix`, `perf`, `ci`, `refactor` | patch (1.5.1 → 1.5.2) | | `feat!`, `fix!` или `BREAKING CHANGE:` в теле | major (1.0.0 → 2.0.0) | -| `docs`, `chore`, `test`, `refactor` | без релиза | +| `docs`, `chore`, `test` | без релиза | + +`refactor` — patch без новых функций: перестройка кода/UI при том же поведении для пользователя. По semver на одном уровне с `fix`, но семантически «мельче» `feat` (не minor). Отдельного суффикса `1.x.y.fix` в semver нет: «fix» в Conventional Commits означает **patch** (третья цифра). Для починки пайплайна без смены продукта — `fix(ci):` или `ci:` (оба дают patch после настройки `.releaserc.json`). diff --git a/web/src/lib/components/network/BirdSettingsForm.svelte b/web/src/lib/components/network/BirdSettingsForm.svelte new file mode 100644 index 0000000..a3e09c4 --- /dev/null +++ b/web/src/lib/components/network/BirdSettingsForm.svelte @@ -0,0 +1,212 @@ + + + + + Control plane + + Глобальные параметры BIRD для pipeline refresh/apply. Сохранение через + PATCH /v1/settings (роль operator). + + + + + + Подстановка в конфиг + + Значения используются при генерации BIRD-конфигурации в pipeline (router id, local AS, + адреса). Пиры и спикеры настраиваются на соседних вкладках. + + + + {#if loading && !loaded} +

Загрузка…

+ {:else if !loaded} + + {:else} +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + {#if hasValidationErrors} +

+ Есть ошибки в полях. Исправьте их, чтобы сохранить изменения. +

+ {/if} + + + {/if} +
+
diff --git a/web/src/lib/components/operations/OperationsSystemSettingsTab.svelte b/web/src/lib/components/operations/OperationsSystemSettingsTab.svelte new file mode 100644 index 0000000..dc75b97 --- /dev/null +++ b/web/src/lib/components/operations/OperationsSystemSettingsTab.svelte @@ -0,0 +1,223 @@ + + +
+ + + Operator-only + + Изменение параметров через PATCH /v1/settings требует роли operator. + При отсутствии прав API вернёт 403. + + + + + + Хранение ревизий + + Автоматическая очистка старых ревизий. Последняя раскатанная ревизия не удаляется. + + + + {#if loading && !loaded} +

Загрузка…

+ {:else if !loaded} + + {:else} + + + + {/if} +
+
+ + + +
+
+ Дополнительные параметры + Произвольные KV-пары в global_settings. +
+ {#if loaded} + + {/if} +
+
+ + {#if !loaded} +

Загрузите настройки выше.

+ {:else if additionalSettings.length === 0} + + {:else} +
+ {#each additionalSettings as entry (entry.id)} +
+ + + +
+ {/each} +
+ {/if} +
+
+ + {#if loaded} + {#if hasValidationErrors} +

+ Есть ошибки в полях. Исправьте их, чтобы сохранить изменения. +

+ {/if} + + + {/if} +
diff --git a/web/src/lib/settings/bird-settings.schema.ts b/web/src/lib/settings/bird-settings.schema.ts new file mode 100644 index 0000000..f7599e5 --- /dev/null +++ b/web/src/lib/settings/bird-settings.schema.ts @@ -0,0 +1,24 @@ +import { z } from 'zod'; +import { optionalIPv4, optionalIPv6 } from './ip-validation.js'; + +export const birdSettingsSchema = z.object({ + bird_router_id: optionalIPv4('router id'), + bird_local_ipv4: optionalIPv4('local IPv4'), + bird_local_ipv6: optionalIPv6('local IPv6'), + bird_local_asn: z.string().refine((v) => v.trim() === '' || /^[1-9]\d*$/.test(v.trim()), { + message: 'ASN должен быть целым числом больше 0' + }), + bird_bgp_source_ipv4: optionalIPv4('BGP source IPv4'), + bird_bgp_source_ipv6: optionalIPv6('BGP source IPv6') +}); + +export type BirdSettingsForm = z.infer; + +export const emptyBirdSettingsForm = (): BirdSettingsForm => ({ + bird_router_id: '', + bird_local_ipv4: '', + bird_local_ipv6: '', + bird_local_asn: '', + bird_bgp_source_ipv4: '', + bird_bgp_source_ipv6: '' +}); diff --git a/web/src/lib/settings/ip-validation.ts b/web/src/lib/settings/ip-validation.ts new file mode 100644 index 0000000..82e2be3 --- /dev/null +++ b/web/src/lib/settings/ip-validation.ts @@ -0,0 +1,47 @@ +import { z } from 'zod'; + +function isValidIPv4(value: string): boolean { + const parts = value.split('.'); + if (parts.length !== 4) return false; + for (const part of parts) { + if (!/^\d{1,3}$/.test(part)) return false; + if (part.length > 1 && part.startsWith('0')) return false; + const n = Number(part); + if (!Number.isInteger(n) || n < 0 || n > 255) return false; + } + return true; +} + +function isValidIPv6(value: string): boolean { + if (!/^[0-9A-Fa-f:.]+$/.test(value)) return false; + if ((value.match(/::/g) ?? []).length > 1) return false; + const hasCompression = value.includes('::'); + const [leftRaw, rightRaw = ''] = value.split('::'); + const left = leftRaw === '' ? [] : leftRaw.split(':'); + const right = rightRaw === '' ? [] : rightRaw.split(':'); + if (left.some((part) => part === '') || right.some((part) => part === '')) return false; + let segments = [...left, ...right]; + let ipv4TailSegments = 0; + const lastSegment = segments.at(-1); + if (lastSegment && lastSegment.includes('.')) { + if (!isValidIPv4(lastSegment)) return false; + segments = segments.slice(0, -1); + ipv4TailSegments = 2; + } + for (const segment of segments) { + if (!/^[0-9A-Fa-f]{1,4}$/.test(segment)) return false; + } + const totalSegments = segments.length + ipv4TailSegments; + if (hasCompression) return totalSegments < 8; + return totalSegments === 8; +} + +export const optionalIPv4 = (label: string) => + z.string().refine((v) => v.trim() === '' || isValidIPv4(v.trim()), { + message: `Введите корректный IPv4 адрес (${label})` + }); + +export const optionalIPv6 = (label: string) => + z.string().refine((v) => v.trim() === '' || isValidIPv6(v.trim()), { + message: `Введите корректный IPv6 адрес (${label})` + }); diff --git a/web/src/lib/settings/revision-settings.schema.ts b/web/src/lib/settings/revision-settings.schema.ts new file mode 100644 index 0000000..3c98a61 --- /dev/null +++ b/web/src/lib/settings/revision-settings.schema.ts @@ -0,0 +1,19 @@ +import { z } from 'zod'; + +export const revisionSettingsSchema = z.object({ + revision_retention_minutes: z.string().refine( + (v) => { + const s = v.trim(); + if (s === '') return true; + const ttl = Number(s); + return /^\d+$/.test(s) && Number.isInteger(ttl) && ttl >= 15 && ttl <= 43200; + }, + { message: 'TTL ревизий должен быть целым числом от 15 до 43200 минут' } + ) +}); + +export type RevisionSettingsForm = z.infer; + +export const emptyRevisionSettingsForm = (): RevisionSettingsForm => ({ + revision_retention_minutes: '' +}); diff --git a/web/src/lib/settings/settings-api.ts b/web/src/lib/settings/settings-api.ts new file mode 100644 index 0000000..5d9188e --- /dev/null +++ b/web/src/lib/settings/settings-api.ts @@ -0,0 +1,89 @@ +import { apiJSON, apiMutate } from '$lib/api/client.js'; +import type { AppSettings } from '$lib/api/types.js'; +import { emptyBirdSettingsForm, type BirdSettingsForm } from './bird-settings.schema.js'; +import { + emptyRevisionSettingsForm, + type RevisionSettingsForm +} from './revision-settings.schema.js'; +import { + BIRD_SETTING_KEYS, + KNOWN_SETTING_KEYS, + NUMERIC_SETTING_KEYS, + type BirdSettingKey, + type KnownSettingKey, + type RevisionSettingKey +} from './settings-known-keys.js'; + +export type AdditionalSettingEntry = { id: number; key: string; value: string }; + +export type PartitionedSettings = { + bird: BirdSettingsForm; + revision: RevisionSettingsForm; + additional: AdditionalSettingEntry[]; +}; + +function parseKnownValue(key: KnownSettingKey, value: unknown): string { + if (NUMERIC_SETTING_KEYS.has(key)) { + if (typeof value === 'number' && Number.isFinite(value)) return String(value); + if (typeof value === 'string') return value; + return ''; + } + if (typeof value === 'string') return value; + return ''; +} + +export function partitionSettings( + settings: AppSettings, + nextId = 1 +): { partitioned: PartitionedSettings; nextId: number } { + const bird = emptyBirdSettingsForm(); + const revision = emptyRevisionSettingsForm(); + const additional: AdditionalSettingEntry[] = []; + let idCounter = nextId; + + for (const [key, value] of Object.entries(settings as Record)) { + if ((BIRD_SETTING_KEYS as readonly string[]).includes(key)) { + bird[key as BirdSettingKey] = parseKnownValue(key as KnownSettingKey, value); + } else if (key === 'revision_retention_minutes') { + revision.revision_retention_minutes = parseKnownValue(key as RevisionSettingKey, value); + } else { + additional.push({ + id: idCounter++, + key, + value: typeof value === 'string' ? value : String(value) + }); + } + } + + return { + partitioned: { bird, revision, additional }, + nextId: idCounter + }; +} + +export async function loadSettings(): Promise { + return apiJSON('/v1/settings'); +} + +export async function patchSettings(payload: Record): Promise { + await apiMutate('/v1/settings', 'PATCH', payload); +} + +export function buildPayloadFromFormFields( + keys: readonly KnownSettingKey[], + form: Record, + errors: Partial> +): Record { + const payload: Record = {}; + for (const key of keys) { + const value = String(form[key] ?? '').trim(); + if (!value || errors[key]?.length) continue; + if (NUMERIC_SETTING_KEYS.has(key)) payload[key] = Number(value); + else payload[key] = value; + } + return payload; +} + +export function isKnownSettingKey(key: string): key is KnownSettingKey { + return (KNOWN_SETTING_KEYS as readonly string[]).includes(key); +} diff --git a/web/src/lib/settings/settings-known-keys.ts b/web/src/lib/settings/settings-known-keys.ts new file mode 100644 index 0000000..63caccd --- /dev/null +++ b/web/src/lib/settings/settings-known-keys.ts @@ -0,0 +1,21 @@ +export const BIRD_SETTING_KEYS = [ + 'bird_router_id', + 'bird_local_ipv4', + 'bird_local_ipv6', + 'bird_local_asn', + 'bird_bgp_source_ipv4', + 'bird_bgp_source_ipv6' +] as const; + +export const REVISION_SETTING_KEYS = ['revision_retention_minutes'] as const; + +export const KNOWN_SETTING_KEYS = [...BIRD_SETTING_KEYS, ...REVISION_SETTING_KEYS] as const; + +export type BirdSettingKey = (typeof BIRD_SETTING_KEYS)[number]; +export type RevisionSettingKey = (typeof REVISION_SETTING_KEYS)[number]; +export type KnownSettingKey = (typeof KNOWN_SETTING_KEYS)[number]; + +export const NUMERIC_SETTING_KEYS = new Set([ + 'bird_local_asn', + 'revision_retention_minutes' +]); diff --git a/web/src/lib/settings/settings-known.schema.ts b/web/src/lib/settings/settings-known.schema.ts index cdcfb1a..1d6aba1 100644 --- a/web/src/lib/settings/settings-known.schema.ts +++ b/web/src/lib/settings/settings-known.schema.ts @@ -1,79 +1,15 @@ import { z } from 'zod'; +import { birdSettingsSchema, emptyBirdSettingsForm } from './bird-settings.schema.js'; +import { emptyRevisionSettingsForm, revisionSettingsSchema } from './revision-settings.schema.js'; -function isValidIPv4(value: string): boolean { - const parts = value.split('.'); - if (parts.length !== 4) return false; - for (const part of parts) { - if (!/^\d{1,3}$/.test(part)) return false; - if (part.length > 1 && part.startsWith('0')) return false; - const n = Number(part); - if (!Number.isInteger(n) || n < 0 || n > 255) return false; - } - return true; -} - -function isValidIPv6(value: string): boolean { - if (!/^[0-9A-Fa-f:.]+$/.test(value)) return false; - if ((value.match(/::/g) ?? []).length > 1) return false; - const hasCompression = value.includes('::'); - const [leftRaw, rightRaw = ''] = value.split('::'); - const left = leftRaw === '' ? [] : leftRaw.split(':'); - const right = rightRaw === '' ? [] : rightRaw.split(':'); - if (left.some((part) => part === '') || right.some((part) => part === '')) return false; - let segments = [...left, ...right]; - let ipv4TailSegments = 0; - const lastSegment = segments.at(-1); - if (lastSegment && lastSegment.includes('.')) { - if (!isValidIPv4(lastSegment)) return false; - segments = segments.slice(0, -1); - ipv4TailSegments = 2; - } - for (const segment of segments) { - if (!/^[0-9A-Fa-f]{1,4}$/.test(segment)) return false; - } - const totalSegments = segments.length + ipv4TailSegments; - if (hasCompression) return totalSegments < 8; - return totalSegments === 8; -} - -const optionalIPv4 = (label: string) => - z.string().refine((v) => v.trim() === '' || isValidIPv4(v.trim()), { - message: `Введите корректный IPv4 адрес (${label})` - }); - -const optionalIPv6 = (label: string) => - z.string().refine((v) => v.trim() === '' || isValidIPv6(v.trim()), { - message: `Введите корректный IPv6 адрес (${label})` - }); - -export const settingsKnownSchema = z.object({ - bird_router_id: optionalIPv4('router id'), - bird_local_ipv4: optionalIPv4('local IPv4'), - bird_local_ipv6: optionalIPv6('local IPv6'), - bird_local_asn: z.string().refine((v) => v.trim() === '' || /^[1-9]\d*$/.test(v.trim()), { - message: 'ASN должен быть целым числом больше 0' - }), - bird_bgp_source_ipv4: optionalIPv4('BGP source IPv4'), - bird_bgp_source_ipv6: optionalIPv6('BGP source IPv6'), - revision_retention_minutes: z.string().refine( - (v) => { - const s = v.trim(); - if (s === '') return true; - const ttl = Number(s); - return /^\d+$/.test(s) && Number.isInteger(ttl) && ttl >= 15 && ttl <= 43200; - }, - { message: 'TTL ревизий должен быть целым числом от 15 до 43200 минут' } - ) -}); +/** @deprecated Используйте birdSettingsSchema и revisionSettingsSchema отдельно. */ +export const settingsKnownSchema = birdSettingsSchema.merge(revisionSettingsSchema); +/** @deprecated Используйте BirdSettingsForm и RevisionSettingsForm. */ export type SettingsKnownForm = z.infer; +/** @deprecated Используйте emptyBirdSettingsForm и emptyRevisionSettingsForm. */ export const emptySettingsKnownForm = (): SettingsKnownForm => ({ - bird_router_id: '', - bird_local_ipv4: '', - bird_local_ipv6: '', - bird_local_asn: '', - bird_bgp_source_ipv4: '', - bird_bgp_source_ipv6: '', - revision_retention_minutes: '' + ...emptyBirdSettingsForm(), + ...emptyRevisionSettingsForm() }); diff --git a/web/src/lib/theme-preferences.svelte.ts b/web/src/lib/theme-preferences.svelte.ts new file mode 100644 index 0000000..05c1393 --- /dev/null +++ b/web/src/lib/theme-preferences.svelte.ts @@ -0,0 +1,28 @@ +import { browser } from '$app/environment'; +import { applyTheme, readTheme, THEME_STORAGE_KEY, type ThemePreference } from './theme.js'; + +class ThemePreferencesState { + pref = $state('system'); + + init(): void { + if (!browser) return; + this.pref = readTheme(); + applyTheme(this.pref); + } + + set(next: ThemePreference): void { + this.pref = next; + } + + persist(): void { + if (!browser) return; + applyTheme(this.pref); + try { + localStorage.setItem(THEME_STORAGE_KEY, this.pref); + } catch { + /* ignore */ + } + } +} + +export const themeState = new ThemePreferencesState(); diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index 96d4991..ddef520 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -5,19 +5,17 @@ import favicon from '$lib/assets/favicon.svg'; import AppLayout from '$lib/ui/app/layout/app-layout.svelte'; import ConfirmDialog from '$lib/ui/patterns/confirm/confirm-dialog.svelte'; - import { applyTheme, readTheme, THEME_STORAGE_KEY, type ThemePreference } from '$lib/theme.js'; + import { applyTheme } from '$lib/theme.js'; + import { themeState } from '$lib/theme-preferences.svelte.js'; import { Toaster } from 'svelte-sonner'; let { children } = $props(); - let themePref = $state('system'); - onMount(() => { - themePref = readTheme(); - applyTheme(themePref); + themeState.init(); const mq = window.matchMedia('(prefers-color-scheme: dark)'); const onOs = () => { - if (themePref === 'system') applyTheme('system'); + if (themeState.pref === 'system') applyTheme('system'); }; mq.addEventListener('change', onOs); return () => mq.removeEventListener('change', onOs); @@ -25,16 +23,11 @@ $effect(() => { if (!browser) return; - applyTheme(themePref); - try { - localStorage.setItem(THEME_STORAGE_KEY, themePref); - } catch { - /* ignore */ - } + themeState.persist(); }); const sonnerTheme = $derived( - themePref === 'system' ? 'system' : themePref === 'dark' ? 'dark' : 'light' + themeState.pref === 'system' ? 'system' : themeState.pref === 'dark' ? 'dark' : 'light' ); @@ -44,4 +37,4 @@ -{@render children()} +{@render children()} diff --git a/web/src/routes/network/+page.svelte b/web/src/routes/network/+page.svelte index f422b84..7fffc9f 100644 --- a/web/src/routes/network/+page.svelte +++ b/web/src/routes/network/+page.svelte @@ -1,32 +1,33 @@
@@ -187,10 +210,11 @@ class="sm:grid-cols-3" /> - + Пиры Спикеры + Control plane @@ -213,5 +237,9 @@ onRefresh={refreshSpeakers} /> + + + +
diff --git a/web/src/routes/operations/+page.svelte b/web/src/routes/operations/+page.svelte index 12bf745..a170ffa 100644 --- a/web/src/routes/operations/+page.svelte +++ b/web/src/routes/operations/+page.svelte @@ -53,6 +53,7 @@ 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, @@ -80,10 +81,10 @@ import Info from '@lucide/svelte/icons/info'; import ArrowRight from '@lucide/svelte/icons/arrow-right'; - type OpsTab = 'revisions' | 'diff' | 'jobs'; + type OpsTab = 'revisions' | 'diff' | 'jobs' | 'system'; function parseOpsTab(value: string | null): OpsTab { - if (value === 'diff' || value === 'jobs') return value; + if (value === 'diff' || value === 'jobs' || value === 'system') return value; return 'revisions'; } @@ -882,12 +883,12 @@ - Три раздела на одной странице + Четыре раздела на одной странице Ревизии — история конфигов и откат; Сравнение — diff префиксов; - Задачи — ingest, apply, rollback. Apply и Reload требуют operator. Сводный - мониторинг BGP — на + Задачи — ingest, apply, rollback; Система — TTL ревизий и + дополнительные KV. Apply и Reload требуют operator. Сводный мониторинг BGP — на . @@ -918,6 +919,7 @@ Ревизии ({revisions.length}) Сравнение Задачи ({jobs.length}) + Система @@ -982,6 +984,10 @@ jobStatusVariant={jobStatusBadgeVariant} /> + + + + diff --git a/web/src/routes/settings/+page.svelte b/web/src/routes/settings/+page.svelte index 9f46148..0662a70 100644 --- a/web/src/routes/settings/+page.svelte +++ b/web/src/routes/settings/+page.svelte @@ -1,110 +1,33 @@ -
+
- -
-
- {#each sectionItems as section (section.id)} - - {/each} -
- -
- {#if activeSection === 'token'} -
-
-

API-ключ

-

- Bearer-токен хранится только в localStorage браузера. Для локального демо с - EVOBGP_DEV_INSECURE=1 - используйте токен dev. -

-
- -
- - -
- - -
- {:else if loadingSettings} -

Загрузка…

- {:else if apiSettings === null} - - {:else} -
- {#if activeSection === 'bird'} -
-

Параметры BIRD

- - - - - - - - - - - - - - - - - - - - - - - - -
- {:else if activeSection === 'revisions'} -
-

Управление ревизиями

- - - - -
- {:else if activeSection === 'additional'} -
-
-

Дополнительные настройки (KV)

- -
- - {#if additionalSettings.length === 0} -

Нет дополнительных параметров.

- {/if} - -
- {#each additionalSettings as entry (entry.id)} -
- - - -
- {/each} -
-
- {/if} - - {#if hasValidationErrors} -

- Есть ошибки в полях. Исправьте их, чтобы сохранить изменения. -

- {/if} - -
- -
-
- {/if} -
-
-
- + + API-ключ - GET/PATCH /v1/settings — глобальные параметры control plane (хранятся - в БД). Требуется роль operator. + Bearer-токен хранится только в localStorage браузера. Для локального демо с + EVOBGP_DEV_INSECURE=1 используйте токен + dev. + +
+ + +
+ + +
+
+ + + + Оформление + + Тема интерфейса. Быстрый переключатель также доступен в боковой панели. + + + + + +