diff --git a/web/src/lib/components/directories/DirectoriesCommunitiesCard.svelte b/web/src/lib/components/directories/DirectoriesCommunitiesCard.svelte
new file mode 100644
index 0000000..330c53f
--- /dev/null
+++ b/web/src/lib/components/directories/DirectoriesCommunitiesCard.svelte
@@ -0,0 +1,188 @@
+
+
+
+
+
+ Сообщества BGP
+ Используются для тегирования префиксов в AS- и CDN-модулях
+
+
+
+
+ c.id}
+ loading={initialLoading || loading}
+ {error}
+ emptyTitle="Нет сообществ BGP"
+ emptyDescription="Создайте первое сообщество для тегирования префиксов."
+ >
+ {#snippet cell({ row: c, column })}
+ {#if column.id === 'community'}
+ {c.community}
+ {:else if column.id === 'title'}
+ {c.title?.trim() || '—'}
+ {:else if column.id === 'id'}
+ {c.id}
+ {:else if column.id === 'actions'}
+
+
openEdit(c)}>
+
+
+
requestDelete(c)}
+ >
+
+
+
+ {/if}
+ {/snippet}
+
+
+
+
+
+
+
+ {editTarget ? 'Редактировать сообщество BGP' : 'Новое сообщество BGP'}
+
+
+
+ (dialogOpen = false)}>Отмена
+
+ {saving ? 'Сохранение…' : editTarget ? 'Сохранить' : 'Создать'}
+
+
+
+
diff --git a/web/src/lib/components/directories/DirectoriesDohProfilesCard.svelte b/web/src/lib/components/directories/DirectoriesDohProfilesCard.svelte
new file mode 100644
index 0000000..380e2d0
--- /dev/null
+++ b/web/src/lib/components/directories/DirectoriesDohProfilesCard.svelte
@@ -0,0 +1,178 @@
+
+
+
+
+
+ DoH профили
+ DNS-over-HTTPS серверы для резолвинга доменных модулей
+
+
+
+
+ d.id}
+ loading={initialLoading || loading}
+ {error}
+ emptyTitle="Нет DoH профилей"
+ emptyDescription="Добавьте DNS-over-HTTPS сервер для доменных модулей."
+ >
+ {#snippet cell({ row: d, column })}
+ {#if column.id === 'url'}
+ {d.url}
+ {:else if column.id === 'timeout_ms'}
+ {d.timeout_ms ?? '—'}
+ {:else if column.id === 'id'}
+ {d.id}
+ {:else if column.id === 'actions'}
+
+
openEdit(d)}>
+
+
+
requestDelete(d)}
+ >
+
+
+
+ {/if}
+ {/snippet}
+
+
+
+
+
+
+
+ {editTarget ? 'Редактировать' : 'Новый'} DoH профиль
+
+
+
+ (dialogOpen = false)}>Отмена
+
+ {saving ? 'Сохранение…' : editTarget ? 'Сохранить' : 'Создать'}
+
+
+
+
diff --git a/web/src/lib/components/network/NetworkPeersCard.svelte b/web/src/lib/components/network/NetworkPeersCard.svelte
new file mode 100644
index 0000000..921b333
--- /dev/null
+++ b/web/src/lib/components/network/NetworkPeersCard.svelte
@@ -0,0 +1,290 @@
+
+
+
+
+
+ BGP-пиры
+ Настройка BGP-соседей и привязка к спикерам
+
+
+
+
+ p.id}
+ loading={initialLoading || loading}
+ {error}
+ emptyTitle="Нет BGP-пиров"
+ emptyDescription="Добавьте первого BGP-соседа для установки сессии."
+ >
+ {#snippet cell({ row: p, column })}
+ {#if column.id === 'name'}
+ {p.name?.trim() || '—'}
+ {:else if column.id === 'neighbor'}
+ {p.neighbor}
+ {:else if column.id === 'remote_asn'}
+ {p.remote_asn ?? '—'}
+ {:else if column.id === 'enabled'}
+
+ setEnabled(p, v)}
+ />
+
+ {:else if column.id === 'session_state'}
+ {p.session_state || '—'}
+ {:else if column.id === 'speaker'}
+ {speakerLabelById(p.bgp_speaker_id)}
+ {:else if column.id === 'actions'}
+
+
openEdit(p)}>
+
+
+
requestDelete(p)}
+ >
+
+
+
+ {/if}
+ {/snippet}
+
+
+
+
+
+
+
+ {editTarget ? 'Редактировать пира' : 'Новый пир'}
+ BGP-сосед для установки сессии
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ form = { ...form, bgp_speaker_id: v || null };
+ }}
+ >
+
+ {form.bgp_speaker_id ? speakerLabelById(form.bgp_speaker_id) : 'Не выбрано'}
+
+
+ Не выбрано
+ {#each speakers as s (s.id)}
+ {s.endpoint} ({s.id.slice(0, 8)}…)
+ {/each}
+
+
+
+
+
+
Включён
+
+ Выключенный пир не попадает в конфиг BIRD до следующей ревизии.
+
+
+
{
+ form = { ...form, enabled: v };
+ }}
+ />
+
+
+
+ (dialogOpen = false)}>Отмена
+
+ {saving ? 'Сохранение…' : editTarget ? 'Сохранить' : 'Создать'}
+
+
+
+
diff --git a/web/src/lib/components/network/NetworkSpeakersCard.svelte b/web/src/lib/components/network/NetworkSpeakersCard.svelte
new file mode 100644
index 0000000..7d7d7b1
--- /dev/null
+++ b/web/src/lib/components/network/NetworkSpeakersCard.svelte
@@ -0,0 +1,177 @@
+
+
+
+
+
+ Спикеры
+ BIRD-агенты, применяющие конфигурацию на нодах
+
+
+
+
+ s.id}
+ loading={initialLoading || loading}
+ {error}
+ emptyTitle="Нет спикеров"
+ emptyDescription="Добавьте BIRD-агент для применения конфигурации."
+ >
+ {#snippet cell({ row: s, column })}
+ {#if column.id === 'endpoint'}
+ {s.endpoint}
+ {:else if column.id === 'role'}
+ {s.role}
+ {:else if column.id === 'last_applied_revision_id'}
+
+ {s.last_applied_revision_id ? s.last_applied_revision_id.slice(0, 8) + '…' : '—'}
+
+ {:else if column.id === 'actions'}
+
+
applySpeaker(s.id)}
+ disabled={applyingId === s.id}
+ >
+
+ {applyingId === s.id ? 'Apply…' : 'Apply'}
+
+
openEdit(s)}>
+
+
+
+ {/if}
+ {/snippet}
+
+
+
+
+
+
+
+ {editTarget ? 'Редактировать спикера' : 'Новый спикер'}
+
+
+
+ (dialogOpen = false)}>Отмена
+
+ {saving ? 'Сохранение…' : editTarget ? 'Сохранить' : 'Создать'}
+
+
+
+
diff --git a/web/src/routes/directories/+page.svelte b/web/src/routes/directories/+page.svelte
index fb264ac..4a89929 100644
--- a/web/src/routes/directories/+page.svelte
+++ b/web/src/routes/directories/+page.svelte
@@ -1,219 +1,221 @@
+ >
+ {#snippet actions()}
+
+
+ Обновить
+
+ {/snippet}
+
+
+
+
+ О справочниках
+
+ Сообщества BGP используются в AS- и CDN-модулях для тегирования префиксов. DoH-профили — в
+ доменных модулях для DNS-over-HTTPS резолвинга. Настройка модулей — на странице
+ Модули .
+
+
+
+
+ {#if initialLoading}
+ {#each Array(3) as _, i (i)}
+
+ {/each}
+ {:else}
+ {#each kpiCards as card (card.id)}
+ {@const Icon = card.icon}
+ {@const a = card.accent}
+
+
+
+
+
+
+
+ {card.label}
+
+ {#if card.href}
+
+
+
+ {/if}
+
+ {card.value}
+
+
+ {card.badge}
+ {card.description}
+
+
+ {/each}
+ {/if}
+
@@ -221,215 +223,24 @@
DoH профили
-
-
-
-
- Сообщества BGP
- Используются для тегирования префиксов
-
-
-
-
-
-
-
- Код сообщества
- Название
- ID
-
-
-
-
- {#each communities as c (c.id)}
-
- {c.community}
- {c.title?.trim() || '—'}
- {c.id}
-
-
-
openCommEdit(c)}
- >
-
(commDeleteTarget = c)}>
-
-
-
- {:else}
-
-
- {commLoading ? 'Загрузка…' : 'Нет записей. Создайте первую.'}
-
-
- {/each}
-
-
-
-
+
-
-
-
-
- DoH профили
- DNS-over-HTTPS серверы для резолвинга доменных модулей
-
-
-
-
-
-
-
- URL
- Таймаут (мс)
- ID
-
-
-
-
- {#each dohProfiles as d (d.id)}
-
- {d.url}
- {d.timeout_ms ?? '—'}
- {d.id}
-
-
-
openDohEdit(d)}
- >
-
(dohDeleteTarget = d)}>
-
-
-
- {:else}
-
-
- {dohLoading ? 'Загрузка…' : 'Нет DoH профилей.'}
-
-
- {/each}
-
-
-
-
+
-
-
-
-
-
- {commEdit ? 'Редактировать сообщество BGP' : 'Новое сообщество BGP'}
-
-
-
- (commDialog = false)}>Отмена
- {commSaving ? 'Сохранение…' : commEdit ? 'Сохранить' : 'Создать'}
-
-
-
-
- {
- if (!v) commDeleteTarget = null;
- }}
->
-
-
- Удалить сообщество «{commDisplay(commDeleteTarget)}»?
- Это приведёт к удалению привязки во всех модулях.
-
-
- (commDeleteTarget = null)}>Отмена
- Удалить
-
-
-
-
-
-
-
-
- {dohEdit ? 'Редактировать' : 'Новый'} DoH профиль
-
-
-
- (dohDialog = false)}>Отмена
- {dohSaving ? 'Сохранение…' : dohEdit ? 'Сохранить' : 'Создать'}
-
-
-
-
- {
- if (!v) dohDeleteTarget = null;
- }}
->
-
-
- Удалить DoH профиль?
- {dohDeleteTarget?.url}
-
-
- (dohDeleteTarget = null)}>Отмена
- Удалить
-
-
-
diff --git a/web/src/routes/network/+page.svelte b/web/src/routes/network/+page.svelte
index 6073d3a..2097814 100644
--- a/web/src/routes/network/+page.svelte
+++ b/web/src/routes/network/+page.svelte
@@ -1,250 +1,214 @@
+ >
+ {#snippet actions()}
+
+
+ Обновить
+
+ {/snippet}
+
+
+
+
+ О сетевой конфигурации
+
+ Пиры привязаны к спикерам (BIRD-агентам). Apply запускает применение ревизии на ноде. Полный
+ список ревизий и задач — на странице
+ Операции .
+
+
+
+
+ {#if initialLoading}
+ {#each Array(3) as _, i (i)}
+
+ {/each}
+ {:else}
+ {#each kpiCards as card (card.id)}
+ {@const Icon = card.icon}
+ {@const a = card.accent}
+
+
+
+
+
+
+ {card.label}
+
+ {card.value}
+
+
+ {card.badge}
+ {card.description}
+
+
+ {/each}
+ {/if}
+
@@ -252,260 +216,25 @@
Спикеры
-
-
-
-
- BGP-пиры
- Настройка BGP-соседей
-
-
-
-
-
-
-
- Имя
- Адрес
- Remote ASN
- Вкл.
- Состояние сессии
- Спикер
-
-
-
-
- {#each peers as p (p.id)}
-
- {p.name?.trim() || '—'}
- {p.neighbor}
- {p.remote_asn ?? '—'}
-
- setPeerEnabled(p, v)}
- />
-
-
- {p.session_state || '—'}
-
- {speakerLabelById(p.bgp_speaker_id)}
-
-
-
openPeerEdit(p)}
- >
-
(peerDeleteTarget = p)}>
-
-
-
- {:else}
-
-
- {peersLoading ? 'Загрузка…' : 'Нет пиров.'}
-
-
- {/each}
-
-
-
-
+
-
-
-
-
- Спикеры
- BIRD-агенты, применяющие конфигурацию
-
-
-
-
-
-
-
- Endpoint
- Роль
- Последняя ревизия
-
-
-
-
- {#each speakers as s (s.id)}
-
- {s.endpoint}
- {s.role}
- {s.last_applied_revision_id
- ? s.last_applied_revision_id.slice(0, 8) + '…'
- : '—'}
-
-
-
applySpeaker(s.id)}
- disabled={applyingId === s.id}
- >
-
- {applyingId === s.id ? 'Apply…' : 'Apply'}
-
-
openSpeakerEdit(s)}
- >
-
-
-
- {:else}
-
-
- {speakersLoading ? 'Загрузка…' : 'Нет спикеров.'}
-
-
- {/each}
-
-
-
-
+
-
-
-
-
-
- {peerEdit ? 'Редактировать пира' : 'Новый пир'}
- BGP-сосед для установки сессии
-
-
-
- Имя пира (опционально)
-
-
-
- Адрес соседа
-
-
-
- Remote ASN
-
-
-
- Спикер (опционально)
- {
- peerForm = { ...peerForm, bgp_speaker_id: v || null };
- }}
- >
-
- {peerForm.bgp_speaker_id ? speakerLabelById(peerForm.bgp_speaker_id) : 'Не выбрано'}
-
-
- Не выбрано
- {#each speakers as s (s.id)}
- {s.endpoint} ({s.id.slice(0, 8)}…)
- {/each}
-
-
-
-
-
-
Включён
-
- Выключенный пир не попадает в конфиг BIRD до следующей ревизии.
-
-
-
{
- peerForm = { ...peerForm, enabled: v };
- }}
- />
-
-
-
- (peerDialog = false)}>Отмена
- {peerSaving ? 'Сохранение…' : peerEdit ? 'Сохранить' : 'Создать'}
-
-
-
-
- {
- if (!v) peerDeleteTarget = null;
- }}
->
-
-
- Удалить пира?
- {peerDeleteTarget?.neighbor}
-
-
- (peerDeleteTarget = null)}>Отмена
- Удалить
-
-
-
-
-
-
-
-
- {speakerEdit ? 'Редактировать спикера' : 'Новый спикер'}
-
-
-
- (speakerDialog = false)}>Отмена
- {speakerSaving ? 'Сохранение…' : speakerEdit ? 'Сохранить' : 'Создать'}
-
-
-