From 0c11ecfa4847822e983c177d9a65b115d26fced7 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Wed, 20 May 2026 12:23:16 +0700 Subject: [PATCH] feat(web): enhance module management UI with new localization and state handling - Added new utility functions for module state representation in Russian, improving localization. - Introduced derived states for counting enabled and disabled modules, enhancing user insights. - Updated module management components to display last updated timestamps and improved descriptions. - Refactored imports to utilize core UI components for better maintainability and consistency. --- .../modules/ModuleAsEntriesCard.svelte | 342 +++ .../modules/ModuleAsEntryDialog.svelte | 126 + .../modules/ModuleCdnSourceDialog.svelte | 259 ++ .../modules/ModuleCdnSourcesCard.svelte | 238 ++ .../modules/ModuleCreateDialog.svelte | 144 ++ .../modules/ModuleDetailHeader.svelte | 57 + .../modules/ModuleDomainEntriesCard.svelte | 303 +++ .../modules/ModuleDomainEntryDialog.svelte | 109 + .../modules/ModuleEditDialog.svelte | 283 +++ .../modules/ModuleIpRangeEntryDialog.svelte | 104 + .../modules/ModuleIpRangesCard.svelte | 311 +++ .../components/modules/ModuleKpiCards.svelte | 155 ++ .../modules/ModuleSummaryCard.svelte | 115 + .../lib/components/modules/module-helpers.ts | 60 + web/src/lib/ui-labels.ts | 26 + web/src/routes/modules/+page.svelte | 305 +-- .../routes/modules/[moduleId]/+page.svelte | 2103 ++--------------- 17 files changed, 2945 insertions(+), 2095 deletions(-) create mode 100644 web/src/lib/components/modules/ModuleAsEntriesCard.svelte create mode 100644 web/src/lib/components/modules/ModuleAsEntryDialog.svelte create mode 100644 web/src/lib/components/modules/ModuleCdnSourceDialog.svelte create mode 100644 web/src/lib/components/modules/ModuleCdnSourcesCard.svelte create mode 100644 web/src/lib/components/modules/ModuleCreateDialog.svelte create mode 100644 web/src/lib/components/modules/ModuleDetailHeader.svelte create mode 100644 web/src/lib/components/modules/ModuleDomainEntriesCard.svelte create mode 100644 web/src/lib/components/modules/ModuleDomainEntryDialog.svelte create mode 100644 web/src/lib/components/modules/ModuleEditDialog.svelte create mode 100644 web/src/lib/components/modules/ModuleIpRangeEntryDialog.svelte create mode 100644 web/src/lib/components/modules/ModuleIpRangesCard.svelte create mode 100644 web/src/lib/components/modules/ModuleKpiCards.svelte create mode 100644 web/src/lib/components/modules/ModuleSummaryCard.svelte create mode 100644 web/src/lib/components/modules/module-helpers.ts diff --git a/web/src/lib/components/modules/ModuleAsEntriesCard.svelte b/web/src/lib/components/modules/ModuleAsEntriesCard.svelte new file mode 100644 index 0000000..b47a619 --- /dev/null +++ b/web/src/lib/components/modules/ModuleAsEntriesCard.svelte @@ -0,0 +1,342 @@ + + + + + + +
+ AS-записи + + Номер AS и community; имя, число префиксов и дата обновляются при успешном refresh + (RIPEstat). + +
+
+ + + + {#if selectedCount > 0} + + {/if} +
+
+ + e.id} + {loading} + emptyTitle="Нет AS-записей" + emptyDescription="Добавьте ASN или импортируйте CSV." + > + {#snippet toolbar()} + {#if entries.length > 0} +
+ toggleAll(v === true)} + aria-label="Выбрать все AS-записи" + /> + Выбрать все +
+ {/if} + {/snippet} + {#snippet cell({ row: entry, column })} + {#if column.id === 'select'} + toggleSelection(entry.id)} + /> + {:else if column.id === 'asn'} + {entry.asn} + {:else if column.id === 'name'} + + {entry.asn_name?.trim() ? entry.asn_name : '—'} + + {:else if column.id === 'prefixes'} + + {entry.prefix_count != null ? entry.prefix_count : '—'} + + {:else if column.id === 'updated'} + + {formatDateTime(entry.asn_resolved_at)} + + {:else if column.id === 'community'} + + {communityLabel(entry.community_id, communities)} + + {:else if column.id === 'actions'} +
+ + +
+ {/if} + {/snippet} +
+
+
+ + { + editTarget = null; + }} +/> diff --git a/web/src/lib/components/modules/ModuleAsEntryDialog.svelte b/web/src/lib/components/modules/ModuleAsEntryDialog.svelte new file mode 100644 index 0000000..d8bc39b --- /dev/null +++ b/web/src/lib/components/modules/ModuleAsEntryDialog.svelte @@ -0,0 +1,126 @@ + + + + + + {edit ? 'Редактировать запись' : 'Новая AS-запись'} + + Номер автономной системы и community для политики анонса. + + +
+
+ + +
+
+ + +
+
+ + + + +
+
diff --git a/web/src/lib/components/modules/ModuleCdnSourceDialog.svelte b/web/src/lib/components/modules/ModuleCdnSourceDialog.svelte new file mode 100644 index 0000000..3f82d2e --- /dev/null +++ b/web/src/lib/components/modules/ModuleCdnSourceDialog.svelte @@ -0,0 +1,259 @@ + + + + + + {edit ? 'Редактировать источник' : 'Новый CDN-источник'} + +
+
+ + +
+
+ + +
+
+ + + {#if form.source_kind === 'json' && !form.prefix_path?.trim()} +

+ Для JSON укажите путь к полям с CIDR; пустой путь может не дать префиксов. +

+ {/if} +
+
+ + +
+
+ + +
+
+
+ + {#if previewError} + {previewError} + {:else if previewOk} + + Всего: {previewTotal}{#if previewTruncated} + (обрезано){/if} + + {/if} +
+ {#if previewItems.length} +
    + {#each previewItems as item, i (`${i}-${item}`)} +
  • {item}
  • + {/each} +
+ {/if} +
+
+ + + + +
+
diff --git a/web/src/lib/components/modules/ModuleCdnSourcesCard.svelte b/web/src/lib/components/modules/ModuleCdnSourcesCard.svelte new file mode 100644 index 0000000..0ec8e02 --- /dev/null +++ b/web/src/lib/components/modules/ModuleCdnSourcesCard.svelte @@ -0,0 +1,238 @@ + + + + +
+ CDN-источники + URL источников для скачивания списков CIDR. +
+
+ + {#if selectedCount > 0} + + {/if} +
+
+ + s.id} + {loading} + emptyTitle="Нет CDN-источников" + emptyDescription="Добавьте URL для загрузки списков CIDR." + > + {#snippet toolbar()} + {#if sources.length > 0} +
+ toggleAll(v === true)} + aria-label="Выбрать все CDN-источники" + /> + Выбрать все +
+ {/if} + {/snippet} + {#snippet cell({ row: src, column })} + {#if column.id === 'select'} + toggleSelection(src.id)} + /> + {:else if column.id === 'url'} + {src.url} + {:else if column.id === 'kind'} +
+ {normalizeCdnSourceKind(src.source_kind)} + {#if src.prefix_path?.trim()} + {src.prefix_path} + {/if} +
+ {:else if column.id === 'community'} + + {communityLabel(src.community_id, communities)} + + {:else if column.id === 'interval'} + + {src.refresh_interval_sec != null ? `${src.refresh_interval_sec}с` : '—'} + + {:else if column.id === 'refreshed'} + + {formatDateTime(src.last_refreshed_at)} + + {:else if column.id === 'actions'} +
+ + +
+ {/if} + {/snippet} +
+
+
+ + { + editTarget = null; + }} +/> diff --git a/web/src/lib/components/modules/ModuleCreateDialog.svelte b/web/src/lib/components/modules/ModuleCreateDialog.svelte new file mode 100644 index 0000000..0cde457 --- /dev/null +++ b/web/src/lib/components/modules/ModuleCreateDialog.svelte @@ -0,0 +1,144 @@ + + + + + + Новый модуль + Создание нового модуля префиксов. + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ +

+ Модуль участвует в сборке ревизий, если включён. +

+
+ { + form = { ...form, enabled: v }; + }} + /> +
+
+ + + + +
+
diff --git a/web/src/lib/components/modules/ModuleDetailHeader.svelte b/web/src/lib/components/modules/ModuleDetailHeader.svelte new file mode 100644 index 0000000..636544e --- /dev/null +++ b/web/src/lib/components/modules/ModuleDetailHeader.svelte @@ -0,0 +1,57 @@ + + +
+ + + {#snippet actions()} +
+ {moduleTypeRu(mod.type)} + + {moduleEnabledRu(!!mod.enabled)} + + + + +
+ {/snippet} +
+
diff --git a/web/src/lib/components/modules/ModuleDomainEntriesCard.svelte b/web/src/lib/components/modules/ModuleDomainEntriesCard.svelte new file mode 100644 index 0000000..88b8783 --- /dev/null +++ b/web/src/lib/components/modules/ModuleDomainEntriesCard.svelte @@ -0,0 +1,303 @@ + + + + + + +
+ Домены + FQDN для резолвинга через DoH. +
+
+ + + + {#if selectedCount > 0} + + {/if} +
+
+ + e.id} + {loading} + emptyTitle="Нет доменов" + emptyDescription="Добавьте FQDN или импортируйте CSV." + > + {#snippet toolbar()} + {#if entries.length > 0} +
+ toggleAll(v === true)} + aria-label="Выбрать все домены" + /> + Выбрать все +
+ {/if} + {/snippet} + {#snippet cell({ row: entry, column })} + {#if column.id === 'select'} + toggleSelection(entry.id)} + /> + {:else if column.id === 'fqdn'} + {entry.fqdn} + {:else if column.id === 'community'} + + {communityLabel(entry.community_id, communities)} + + {:else if column.id === 'actions'} +
+ + +
+ {/if} + {/snippet} +
+
+
+ + { + editTarget = null; + }} +/> diff --git a/web/src/lib/components/modules/ModuleDomainEntryDialog.svelte b/web/src/lib/components/modules/ModuleDomainEntryDialog.svelte new file mode 100644 index 0000000..5aca8b3 --- /dev/null +++ b/web/src/lib/components/modules/ModuleDomainEntryDialog.svelte @@ -0,0 +1,109 @@ + + + + + + {edit ? 'Редактировать домен' : 'Новый домен'} + +
+
+ + +
+
+ + +
+
+ + + + +
+
diff --git a/web/src/lib/components/modules/ModuleEditDialog.svelte b/web/src/lib/components/modules/ModuleEditDialog.svelte new file mode 100644 index 0000000..9d6e75f --- /dev/null +++ b/web/src/lib/components/modules/ModuleEditDialog.svelte @@ -0,0 +1,283 @@ + + + + + + Редактировать модуль + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + + +
+ {#if mod.type === 'DOMAINS'} +
+ + +

+ {dohPolicyOptions.find( + (o) => o.value === (editForm.doh_resolver_policy ?? 'primary_only') + )?.hint} +

+
+
+ +

+ Порядок выбора = порядок в списке (сверху вниз). +

+
+ {#each dohProfiles as d (d.id)} + + {:else} +

Нет профилей — создайте в справочниках.

+ {/each} +
+ +
+ {/if} +
+
+ +

+ Отключённые модули не участвуют в обновлении конфигурации. +

+
+ { + editForm = { ...editForm, enabled: v }; + }} + /> +
+
+ + + + +
+
diff --git a/web/src/lib/components/modules/ModuleIpRangeEntryDialog.svelte b/web/src/lib/components/modules/ModuleIpRangeEntryDialog.svelte new file mode 100644 index 0000000..135f958 --- /dev/null +++ b/web/src/lib/components/modules/ModuleIpRangeEntryDialog.svelte @@ -0,0 +1,104 @@ + + + + + + {edit ? 'Редактировать диапазон' : 'Новый IP-диапазон'} + +
+
+ + +
+
+ + +
+
+ + + + +
+
diff --git a/web/src/lib/components/modules/ModuleIpRangesCard.svelte b/web/src/lib/components/modules/ModuleIpRangesCard.svelte new file mode 100644 index 0000000..acb1b02 --- /dev/null +++ b/web/src/lib/components/modules/ModuleIpRangesCard.svelte @@ -0,0 +1,311 @@ + + + + + + +
+ IP-диапазоны + Статические CIDR для анонса. +
+
+ + + + {#if selectedCount > 0} + + {/if} +
+
+ + e.id} + {loading} + emptyTitle="Нет диапазонов" + emptyDescription="Добавьте CIDR или импортируйте CSV." + > + {#snippet toolbar()} + {#if entries.length > 0} +
+ toggleAll(v === true)} + aria-label="Выбрать все диапазоны" + /> + Выбрать все +
+ {/if} + {/snippet} + {#snippet cell({ row: entry, column })} + {#if column.id === 'select'} + toggleSelection(entry.id)} + /> + {:else if column.id === 'prefix'} + {entry.prefix} + {:else if column.id === 'community'} + + {communityLabel(entry.community_id, communities)} + + {:else if column.id === 'actions'} +
+ + +
+ {/if} + {/snippet} +
+
+
+ + { + editTarget = null; + }} +/> diff --git a/web/src/lib/components/modules/ModuleKpiCards.svelte b/web/src/lib/components/modules/ModuleKpiCards.svelte new file mode 100644 index 0000000..e3e3a66 --- /dev/null +++ b/web/src/lib/components/modules/ModuleKpiCards.svelte @@ -0,0 +1,155 @@ + + +
+ {#if loading} + {#each Array(5) as _, i (i)} + + {/each} + {:else} + {#each kpiCards as card (card.id)} + {@const Icon = card.icon} + {@const a = card.accent} + + +

+ + {card.label} +

+ + {card.value} + +
+ +

{card.description}

+
+
+ {/each} + {/if} +
diff --git a/web/src/lib/components/modules/ModuleSummaryCard.svelte b/web/src/lib/components/modules/ModuleSummaryCard.svelte new file mode 100644 index 0000000..a0b920e --- /dev/null +++ b/web/src/lib/components/modules/ModuleSummaryCard.svelte @@ -0,0 +1,115 @@ + + + + + Операционный отчёт модуля + + Читаемая сводка по данным модуля: источники, объёмы и ожидаемый результат для + refresh/агрегации. + + + + {#if mod.type === 'DOMAINS'} +
+

Домены и ожидаемые IP

+

+ После refresh домены резолвятся в IP и конвертируются в префиксы. +

+
+ {#each domainEntries.slice(0, 8) as entry (entry.id)} +

{entry.fqdn}

+ {:else} +

Нет доменов

+ {/each} + {#if domainEntries.length > 8} +

…и ещё {domainEntries.length - 8}

+ {/if} +
+
+ {:else if mod.type === 'AS_PREFIXES'} +
+

ASN и число полученных префиксов

+

+ Счётчик префиксов обновляется после успешного refresh (RIPEstat). +

+
+

+ Всего ASN: {asEntries.length} +

+

+ Сумма префиксов: {asPrefixTotal} +

+
+
+ {:else if mod.type === 'CDN_CIDRS'} +
+

CDN ссылки и импортируемые префиксы

+

+ Каждый URL поставляет список CIDR для агрегации. +

+
+ {#each cdnSources.slice(0, 6) as src (src.id)} +

{src.url}

+ {:else} +

Нет CDN источников

+ {/each} + {#if cdnSources.length > 6} +

…и ещё {cdnSources.length - 6}

+ {/if} +
+
+ {:else if mod.type === 'IP_RANGES'} +
+

IP ranges для агрегации

+

+ Статические CIDR, которые попадают в итоговую ревизию. +

+
+ {#each ipEntries.slice(0, 8) as entry (entry.id)} +

{entry.prefix}

+ {:else} +

Нет диапазонов

+ {/each} + {#if ipEntries.length > 8} +

…и ещё {ipEntries.length - 8}

+ {/if} +
+
+ {/if} +
+

Результат операции

+

+ Подробный результат по конкретному запуску refresh смотрите в Операции → Задачи → + module_refresh: там отображаются источники, количество префиксов и итог агрегации. +

+ +
+
+
diff --git a/web/src/lib/components/modules/module-helpers.ts b/web/src/lib/components/modules/module-helpers.ts new file mode 100644 index 0000000..f8288f6 --- /dev/null +++ b/web/src/lib/components/modules/module-helpers.ts @@ -0,0 +1,60 @@ +import type { BgpCommunity, DohProfile, ModuleRow } from '$lib/api/types.js'; + +export const NONE_OPTION = '__none__'; + +export function supportsCsvIO(type: ModuleRow['type'] | null | undefined): boolean { + return type === 'AS_PREFIXES' || type === 'DOMAINS' || type === 'IP_RANGES'; +} + +export function sanitizeFilenamePart(v: string): string { + const cleaned = v + .trim() + .toLowerCase() + .replace(/[^a-z0-9._-]+/g, '-') + .replace(/-+/g, '-') + .replace(/^[-_.]+|[-_.]+$/g, ''); + return cleaned || 'module'; +} + +export function communityLabel(id: string | null, communities: BgpCommunity[]): string { + if (!id) return '—'; + const c = communities.find((x) => x.id === id); + if (!c) return id.slice(0, 8) + '…'; + const t = c.title?.trim(); + return t || c.community; +} + +export function communityOptionLabel(c: BgpCommunity): string { + const t = c.title?.trim(); + return t || c.community; +} + +export function nullableSelectValue(value: string | null | undefined): string { + if (value === null || value === undefined || value === '') return NONE_OPTION; + return value; +} + +export function fromNullableSelect(value: string): string | null { + if (value === NONE_OPTION || value === '') return null; + return value; +} + +export function moduleDohProfileIds(modRow: ModuleRow | null): string[] { + if (!modRow) return []; + if (modRow.doh_profile_ids?.length) return modRow.doh_profile_ids; + return modRow.doh_profile_id ? [modRow.doh_profile_id] : []; +} + +export function dohProfileLabel(id: string, dohProfiles: DohProfile[]): string { + const p = dohProfiles.find((d) => d.id === id); + return p ? (p.name?.trim() ? `${p.name} (${p.url})` : p.url) : id.slice(0, 8) + '…'; +} + +export function normalizeCdnSourceKind(k: string): 'plaintext' | 'json' { + return k.trim().toLowerCase() === 'json' ? 'json' : 'plaintext'; +} + +export function syncSelection(selected: Set, existingIds: string[]): Set { + const validIds = new Set(existingIds); + return new Set([...selected].filter((id) => validIds.has(id))); +} diff --git a/web/src/lib/ui-labels.ts b/web/src/lib/ui-labels.ts index 79ca37d..3d74b04 100644 --- a/web/src/lib/ui-labels.ts +++ b/web/src/lib/ui-labels.ts @@ -43,6 +43,32 @@ export function jobKindFilterRu(kind: string): string { } } +/** Включён / выключен модуль (поле enabled). */ +export function moduleEnabledRu(enabled: boolean): string { + return enabled ? 'Вкл' : 'Выкл'; +} + +/** Variant Badge для enabled модуля. */ +export function moduleEnabledBadgeVariant( + enabled: boolean +): 'default' | 'secondary' | 'outline' | 'destructive' { + return enabled ? 'default' : 'secondary'; +} + +/** Политика DoH-резолвера для модулей DOMAINS. */ +export function dohPolicyRu(policy: string | null | undefined): string { + switch (policy) { + case 'primary_only': + return 'Только первый'; + case 'failover': + return 'Резервирование'; + case 'union': + return 'Объединение'; + default: + return 'Только первый'; + } +} + export function moduleTypeRu(type: string): string { switch (type) { case 'AS_PREFIXES': diff --git a/web/src/routes/modules/+page.svelte b/web/src/routes/modules/+page.svelte index 20b0944..fcda6a4 100644 --- a/web/src/routes/modules/+page.svelte +++ b/web/src/routes/modules/+page.svelte @@ -1,70 +1,105 @@ {#if loadingMod} -
Загрузка…
+
+ +
{:else if mod} - {@const m = mod} -
- + (editDialog = true)} + onDelete={requestDeleteMod} /> -
- - - {#snippet actions()} -
- {moduleTypeRu(m.type)} - {#if m.enabled} - вкл - {:else} - выкл - {/if} - - - -
- {/snippet} -
-
- -
- -

- Приоритет -

-

{mod.priority}

-
- -

- Интервал -

-

- {moduleIntervalLabel(mod)} -

-
- -

- DoH -

- {#if mod.type === 'DOMAINS'} -

{dohPolicyLabel(mod.doh_resolver_policy)}

-

- {#if moduleDohProfileIds(mod).length} - {moduleDohProfileIds(mod).map(dohProfileLabel).join('; ')} - {:else} - Системный DNS - {/if} -

- {:else} -

- {/if} -
- -

- Community по умолч. -

-

{communityLabel(mod.default_community_id)}

-
- -

- Последнее обновление -

-

- {formatDateTime(mod.last_refreshed_at)} -

-
-
+ + + {moduleTypeRu(mod.type)} + + {typeAlertText} + Результат refresh — в + . + + - - - Операционный отчёт модуля - - Читаемая сводка по данным модуля: источники, объёмы и ожидаемый результат для - refresh/агрегации. - - - - {#if mod.type === 'DOMAINS'} -
-

Домены и ожидаемые IP

-

- После refresh домены резолвятся в IP и конвертируются в префиксы. -

-
- {#each domainEntries.slice(0, 8) as entry (entry.id)} -

{entry.fqdn}

- {:else} -

Нет доменов

- {/each} -
-
- {:else if mod.type === 'AS_PREFIXES'} -
-

ASN и число полученных префиксов

-

- Счётчик префиксов обновляется после успешного refresh. -

-
-

- Всего ASN: {asEntries.length} -

-

- Сумма префиксов: {asPrefixTotal} -

-
-
- {:else if mod.type === 'CDN_CIDRS'} -
-

CDN ссылки и импортируемые префиксы

-

- Каждый URL поставляет список CIDR для агрегации. -

-
- {#each cdnSources.slice(0, 6) as src (src.id)} -

{src.url}

- {:else} -

Нет CDN источников

- {/each} -
-
- {:else if mod.type === 'IP_RANGES'} -
-

IP ranges для агрегации

-

- Статические CIDR, которые попадают в итоговую ревизию. -

-
- {#each ipEntries.slice(0, 8) as entry (entry.id)} -

{entry.prefix}

- {:else} -

Нет диапазонов

- {/each} -
-
- {/if} -
-

Результат операции

-

- Подробный результат по конкретному запуску refresh смотрите в `Операции -> Задачи -> - module_refresh`: там отображаются источники, количество префиксов и итог агрегации. -

-
-
-
+ - - {#if mod.type === 'AS_PREFIXES'} - - -
- AS-записи - - Номер AS и community; имя, число префиксов и дата обновляются при успешном обновлении - модуля (RIPEstat) - -
-
- - - - -
-
- - - - - - - toggleAllAs((event.currentTarget as HTMLInputElement).checked)} - /> - - ASN - Название AS - Префиксов - Обновлено - Community - - - - - {#each asEntries as entry (entry.id)} - - - toggleAsSelection(entry.id)} - /> - - {entry.asn} - - {entry.asn_name?.trim() ? entry.asn_name : '—'} - - - {entry.prefix_count != null && entry.prefix_count !== undefined - ? entry.prefix_count - : '—'} - - - {entry.asn_resolved_at - ? new Date(entry.asn_resolved_at).toLocaleString('ru-RU') - : '—'} - - {communityLabel(entry.community_id)} - -
- - -
-
-
- {:else} - - Нет записей - - {/each} -
-
-
-
- {:else if mod.type === 'CDN_CIDRS'} - - -
- CDN-источники - URL источников для скачивания списков CIDR -
-
- - -
-
- - - - - - - toggleAllCdn((event.currentTarget as HTMLInputElement).checked)} - /> - - URL - Тип - Community - Интервал - Последнее обновление - - - - - {#each cdnSources as src (src.id)} - - - toggleCdnSelection(src.id)} - /> - - {src.url} - -
- {normalizeCdnSourceKind(src.source_kind)} - {#if src.prefix_path?.trim()} - {src.prefix_path} - {/if} -
-
- {communityLabel(src.community_id)} - - {src.refresh_interval_sec !== null && src.refresh_interval_sec !== undefined - ? `${src.refresh_interval_sec}с` - : '—'} - - - {formatDateTime(src.last_refreshed_at)} - - -
- - -
-
-
- {:else} - - Нет источников - - {/each} -
-
-
-
- {:else if mod.type === 'DOMAINS'} - - -
- Домены - FQDN для резолвинга через DoH -
-
- - - - -
-
- - - - - - - toggleAllDomain((event.currentTarget as HTMLInputElement).checked)} - /> - - FQDN - Community - - - - - {#each domainEntries as entry (entry.id)} - - - toggleDomainSelection(entry.id)} - /> - - {entry.fqdn} - {communityLabel(entry.community_id)} - -
- - -
-
-
- {:else} - - Нет доменов - - {/each} -
-
-
-
- {:else if mod.type === 'IP_RANGES'} - - -
- IP-диапазоны - Статические CIDR для анонса -
-
- - - - -
-
- - - - - - - toggleAllIp((event.currentTarget as HTMLInputElement).checked)} - /> - - Префикс (CIDR) - Community - - - - - {#each ipEntries as entry (entry.id)} - - - toggleIpSelection(entry.id)} - /> - - {entry.prefix} - {communityLabel(entry.community_id)} - -
- - -
-
-
- {:else} - - Нет диапазонов - - {/each} -
-
-
-
+ + + {#if mod.type === 'AS_PREFIXES' && moduleId} + + {:else if mod.type === 'CDN_CIDRS' && moduleId} + + {:else if mod.type === 'DOMAINS' && moduleId} + + {:else if mod.type === 'IP_RANGES' && moduleId} + {/if}
+ + {#if moduleId} + {}} + /> + {/if} {:else} -

Модуль не найден

+ + {#snippet action()} + + {/snippet} + {/if} - - - - - - Редактировать модуль - -
-
- - -
-
-
- - -
-
- - -
-
-
- - -
-
- - - -
- {#if mod?.type === 'DOMAINS'} -
- - -

- {dohPolicyOptions.find( - (o) => o.value === (editForm.doh_resolver_policy ?? 'primary_only') - )?.hint} -

-
-
- -

- Порядок выбора = порядок в списке (сверху вниз). -

-
- {#each dohProfiles as d (d.id)} - - {:else} -

Нет профилей — создайте в справочниках.

- {/each} -
- -
- {/if} -
-
- -

- Отключённые модули не участвуют в обновлении конфигурации. -

-
- { - editForm = { ...editForm, enabled: v }; - }} - /> -
-
- - - - -
-
- - - - - - Удалить модуль «{mod?.name}»? - Это действие необратимо. Модуль и все его записи будут удалены. - - - Отмена - - {deletingMod ? 'Удаление…' : 'Удалить'} - - - - - - - - - - {asEdit ? 'Редактировать запись' : 'Новая AS-запись'} - Номер автономной системы и community для политики анонса. - -
-
- - -
-
- - -
-
- - - - -
-
- - { - if (!v) asDeleteTarget = null; - }} -> - - - Удалить запись? - ASN: {asDeleteTarget?.asn ?? '—'} - - - (asDeleteTarget = null)}>Отмена - Удалить - - - - - - - - Удалить выбранные AS-записи? - Будет удалено: {selectedAsCount} - - - Отмена - - {deletingAsBulk ? 'Удаление…' : 'Удалить'} - - - - - - - { - if (!open) clearCdnPreview(); - }} -> - - - {cdnEdit ? 'Редактировать источник' : 'Новый CDN-источник'} - -
-
- - -
-
- - -
-
- - - {#if cdnForm.source_kind === 'json' && !cdnForm.prefix_path?.trim()} -

- Для JSON укажите путь к полям с CIDR; пустой путь может не дать префиксов. -

- {/if} -
-
- - -
-
- - -
-
-
- - {#if cdnPreviewError} - {cdnPreviewError} - {:else if cdnPreviewOk} - - Всего: {cdnPreviewTotal}{#if cdnPreviewTruncated} - (обрезано){/if} - - {/if} -
- {#if cdnPreviewItems.length} -
    - {#each cdnPreviewItems as item, i (`${i}-${item}`)} -
  • {item}
  • - {/each} -
- {/if} -
-
- - - - -
-
- - { - if (!v) cdnDeleteTarget = null; - }} -> - - - Удалить CDN-источник? - {cdnDeleteTarget?.url} - - - (cdnDeleteTarget = null)}>Отмена - Удалить - - - - - - - - Удалить выбранные CDN-источники? - Будет удалено: {selectedCdnCount} - - - Отмена - - {deletingCdnBulk ? 'Удаление…' : 'Удалить'} - - - - - - - - - - {domainEdit ? 'Редактировать домен' : 'Новый домен'} - -
-
- - -
-
- - -
-
- - - - -
-
- - { - if (!v) domainDeleteTarget = null; - }} -> - - - Удалить домен? - {domainDeleteTarget?.fqdn} - - - (domainDeleteTarget = null)}>Отмена - Удалить - - - - - - - - Удалить выбранные домены? - Будет удалено: {selectedDomainCount} - - - Отмена - - {deletingDomainBulk ? 'Удаление…' : 'Удалить'} - - - - - - - - - - {ipEdit ? 'Редактировать диапазон' : 'Новый IP-диапазон'} - -
-
- - -
-
- - -
-
- - - - -
-
- - { - if (!v) ipDeleteTarget = null; - }} -> - - - Удалить диапазон? - {ipDeleteTarget?.prefix} - - - (ipDeleteTarget = null)}>Отмена - Удалить - - - - - - - - Удалить выбранные диапазоны? - Будет удалено: {selectedIpCount} - - - Отмена - - {deletingIpBulk ? 'Удаление…' : 'Удалить'} - - - -