diff --git a/web/src/lib/api/client.ts b/web/src/lib/api/client.ts index fac96b0..ab3bfaf 100644 --- a/web/src/lib/api/client.ts +++ b/web/src/lib/api/client.ts @@ -1,5 +1,14 @@ import { browser } from '$app/environment'; -import type { JobRow } from './types.js'; +import type { + AsEntriesResponse, + CdnSourcesResponse, + DomainEntriesResponse, + IpRangeEntriesResponse, + JobRow, + ModuleType, + RevisionPrefix, + RevisionPrefixesResponse +} from './types.js'; export const TOKEN_STORAGE_KEY = 'evobgp_api_token'; @@ -122,3 +131,55 @@ export async function waitForJob( } throw new Error(`Таймаут ожидания задачи ${jobId}`); } + +export async function apiPageAll(path: string, limit = 500): Promise { + const items: T[] = []; + let cursor: string | null = null; + while (true) { + const [basePath, rawQuery = ''] = path.split('?'); + const query = new URLSearchParams(rawQuery); + if (!query.has('limit')) query.set('limit', String(limit)); + if (cursor) query.set('cursor', cursor); + else query.delete('cursor'); + const page = await apiJSON<{ items?: T[]; next_cursor?: string | null; has_more?: boolean }>( + `${basePath}?${query.toString()}` + ); + items.push(...(page.items ?? [])); + if (!page.has_more || !page.next_cursor) break; + cursor = page.next_cursor; + } + return items; +} + +export async function fetchRevisionPrefixesAll(revisionId: string): Promise { + const items = await apiPageAll(`/v1/revisions/${revisionId}/prefixes`); + return items; +} + +export async function fetchModuleSourceCatalog(moduleId: string, moduleType: ModuleType) { + if (moduleType === 'DOMAINS') { + const entries = await apiPageAll( + `/v1/modules/${moduleId}/domain-entries` + ); + return { domains: entries }; + } + if (moduleType === 'AS_PREFIXES') { + const entries = await apiPageAll( + `/v1/modules/${moduleId}/as-entries` + ); + return { asns: entries }; + } + if (moduleType === 'CDN_CIDRS') { + const entries = await apiPageAll( + `/v1/modules/${moduleId}/cdn-sources` + ); + return { cdnSources: entries }; + } + if (moduleType === 'IP_RANGES') { + const entries = await apiPageAll( + `/v1/modules/${moduleId}/ip-range-entries` + ); + return { ipRanges: entries }; + } + return {}; +} diff --git a/web/src/lib/api/types.ts b/web/src/lib/api/types.ts index 04723cf..da406a1 100644 --- a/web/src/lib/api/types.ts +++ b/web/src/lib/api/types.ts @@ -179,6 +179,8 @@ export type RevisionsResponse = Page; export type RevisionPrefix = { /** Обычно CIDR; для AS-модуля в снимке ревизии — строка вида `as:<номер_asn>`. */ prefix: string; + /** Источник материализации (например, domain:, as:, cdn:, ip_range). */ + source?: string; community_id?: string | null; }; export type RevisionPrefixesResponse = Page; diff --git a/web/src/routes/modules/[moduleId]/+page.svelte b/web/src/routes/modules/[moduleId]/+page.svelte index e9f134b..b9b8389 100644 --- a/web/src/routes/modules/[moduleId]/+page.svelte +++ b/web/src/routes/modules/[moduleId]/+page.svelte @@ -725,6 +725,10 @@ default: return 'as'; } }); + + const asPrefixTotal = $derived( + asEntries.reduce((acc, entry) => acc + (entry.prefix_count ?? 0), 0) + ); {#if loadingMod} @@ -795,6 +799,70 @@ + + + Операционный отчёт модуля + + Читаемая сводка по данным модуля: источники, объёмы и ожидаемый результат для 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'} @@ -1189,7 +1257,7 @@ Не выбрано - {#each communities as c} + {#each communities as c (c.id)} {communityOptionLabel(c)} {/each} @@ -1203,7 +1271,7 @@ Не выбрано - {#each dohProfiles as d} + {#each dohProfiles as d (d.id)} {d.url} {/each} @@ -1271,7 +1339,7 @@ Не выбрано - {#each communities as c} + {#each communities as c (c.id)} {communityOptionLabel(c)} {/each} @@ -1368,7 +1436,7 @@ Не выбрано - {#each communities as c} + {#each communities as c (c.id)} {communityOptionLabel(c)} {/each} @@ -1455,7 +1523,7 @@ Не выбрано - {#each communities as c} + {#each communities as c (c.id)} {communityOptionLabel(c)} {/each} @@ -1515,7 +1583,7 @@ {ipForm.community_id ? communityLabel(ipForm.community_id) : 'Выберите community'} - {#each communities as c} + {#each communities as c (c.id)} {communityOptionLabel(c)} {/each} diff --git a/web/src/routes/operations/+page.svelte b/web/src/routes/operations/+page.svelte index f9b5346..0ae48c4 100644 --- a/web/src/routes/operations/+page.svelte +++ b/web/src/routes/operations/+page.svelte @@ -1,12 +1,21 @@
@@ -84,6 +88,21 @@
+
+ +

Всего задач

+

{jobs.length}

+
+ +

В работе

+

{runningJobsCount}

+
+ +

С ошибкой

+

{failedJobsCount}

+
+
+ Модули @@ -129,7 +148,7 @@ Последние задачи - GET /v1/jobs + Задачи разложены по статусу и времени запуска. @@ -160,4 +179,35 @@
+ + + + Операции обновления модулей + Отдельная лента задач `module_refresh` для контроля по модулям. + + + + + + Статус + Создана + revision_id + + + + {#each refreshJobs as j (j.job_id)} + + {j.status} + {j.created_at ? new Date(j.created_at).toLocaleString('ru') : '—'} + {typeof j.meta?.revision_id === 'string' ? `${j.meta.revision_id.slice(0, 12)}…` : '—'} + + {:else} + + Нет задач module_refresh + + {/each} + +
+
+