From a2cbfacd453fd6a212f4c00bf702ae6dd12db587 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Mon, 6 Apr 2026 14:52:29 +0700 Subject: [PATCH] feat: update BGP community structure and related documentation. Refactor BGP community handling by renaming fields from 'name' and 'kind' to 'community' and 'title' across the codebase. Update OpenAPI specifications, database interactions, and UI components to reflect these changes, enhancing clarity and consistency in community management. --- AGENTS.md | 49 +++++++++++++++++++ README.md | 1 + docs/README.md | 1 + docs/openapi.yaml | 16 +++--- internal/httpapi/routes_crud.go | 2 +- internal/pipeline/bird_materialize.go | 2 +- internal/pipeline/refresh.go | 19 ++++++- internal/pipeline/refresh_aggregate_test.go | 47 +++++++++++++----- internal/repository/postgres.go | 32 +++++++----- internal/repository/postgres_seed.go | 2 +- internal/store/backend.go | 8 +-- internal/store/memory.go | 2 +- internal/store/memory_crud.go | 12 ++--- .../000005_bgp_community_title.down.sql | 9 ++++ .../000005_bgp_community_title.up.sql | 10 ++++ .../000005_bgp_community_title.down.sql | 5 ++ .../sqlite/000005_bgp_community_title.up.sql | 8 +++ web/src/lib/api/types.ts | 8 +-- web/src/routes/directories/+page.svelte | 39 +++++++-------- .../routes/modules/[moduleId]/+page.svelte | 42 +++++++++------- 20 files changed, 226 insertions(+), 88 deletions(-) create mode 100644 AGENTS.md create mode 100644 migrations/postgres/000005_bgp_community_title.down.sql create mode 100644 migrations/postgres/000005_bgp_community_title.up.sql create mode 100644 migrations/sqlite/000005_bgp_community_title.down.sql create mode 100644 migrations/sqlite/000005_bgp_community_title.up.sql diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d0b5c1c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,49 @@ +# Руководство для ИИ-агентов (экономия контекста) + +Краткие ориентиры по репозиторию **EvoBGP**, чтобы не тратить токены на полное сканирование дерева и повторное чтение одних и тех же файлов. + +## С чего начать (минимум чтения) + +1. **[docs/README.md](docs/README.md)** — оглавление и роли читателя. +2. **[docs/architecture.md](docs/architecture.md)** — компоненты `cmd/`, карта `internal/`, потоки данных (одного этого файла обычно достаточно для ориентации). +3. Задача-специфично: [docs/api.md](docs/api.md), [docs/access.md](docs/access.md), [web/README.md](web/README.md) — только если меняете API, доступ или фронт. + +Источник правды по HTTP-контракту: **[docs/openapi.yaml](docs/openapi.yaml)**. Не дублируйте длинные фрагменты спецификации в ответах — ссылайтесь на путь и тег/операцию. + +## Карта кода (куда смотреть) + +| Область | Где искать | +|---------|------------| +| REST, auth, CORS | `internal/httpapi/` | +| Бизнес-слой и абстракция хранилища | `internal/store/` | +| PostgreSQL | `internal/repository/`, `internal/db/`, `migrations/` | +| Фоновые задачи | `internal/jobs/` | +| Цепочка refresh модуля (ingest+render, BIRD preview) | `internal/pipeline/` | +| Конфиг BIRD, `birdc` | `internal/birdfmt/`, `internal/birddeploy/` | +| Бандлы и подписи | `internal/bundle/`, `internal/signing/` | +| Точки входа процессов | `cmd/*/` | +| Веб (SvelteKit) | `web/` | +| Compose, деплой | `deploy/compose/` | + +Точки входа бинарников и их роли — в таблице в начале [docs/architecture.md](docs/architecture.md). + +## Как не раздувать контекст + +- **Сначала узкий поиск:** `grep`/поиск по символу или короткий семантический запрос по одной папке (`internal/httpapi/`, `internal/pipeline/`, …), а не чтение всех `.go` подряд. +- **Читайте файлы целиком только при необходимости:** большие файлы — с `offset`/`limit` или по найденным строкам. +- **Не подтягивайте в контекст:** `web/node_modules/`, сгенерированные артефакты сборки, бинарники, полный `openapi.html`, если достаточно `openapi.yaml`. +- **Повторное использование:** если [docs/architecture.md](docs/architecture.md) уже описывает поток — не пересказывайте его длинно; укажите документ и конкретный подпункт задачи. +- **Длинные планы:** `.cursor/plans/*.plan.md` — для истории решений; для навигации пользователю достаточно `docs/`; не читайте план целиком без причины. + +## Команды и среда + +- Консоль пользователя: **PowerShell**; пути в стиле `deploy\compose`. +- Быстрый старт и переменные: [docs/quickstart.md](docs/quickstart.md), [README.md](README.md). + +## Язык документации проекта + +Пользовательская документация в `docs/` — преимущественно на русском. Комментарии и имена в коде — в существующем стиле репозитория. + +## Svelte / фронтенд + +При правках `web/**/*.svelte` или Svelte-модулей следуйте навыкам/инструментам проекта (официальный Svelte MCP и скиллы Cursor, если подключены). diff --git a/README.md b/README.md index 0909095..900289f 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Control plane для управления префиксами, модулями | Документ | Содержание | |----------|------------| +| [AGENTS.md](AGENTS.md) | Краткая карта репозитория и советы для ИИ-агентов (экономия контекста) | | [docs/README.md](docs/README.md) | Оглавление и навигация по разделам | | [docs/overview.md](docs/overview.md) | Ключевые возможности продукта | | [docs/quickstart.md](docs/quickstart.md) | Быстрый запуск (Docker, локально, фронтенд) | diff --git a/docs/README.md b/docs/README.md index 5015409..68912ea 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,6 +4,7 @@ ## По роли читателя +- **ИИ-агент / ассистент в репозитории** — [../AGENTS.md](../AGENTS.md): с чего начать чтение, карта `internal/` и `cmd/`, что не тащить в контекст. - **Оператор / DevOps** — [quickstart.md](quickstart.md), [architecture.md](architecture.md), [access.md](access.md), [deploy/compose/docker-compose.yaml](../deploy/compose/docker-compose.yaml). - **Разработчик бэкенда или интегратор API** — [api.md](api.md), [access.md](access.md), [openapi.yaml](openapi.yaml), исходники маршрутов в `internal/httpapi/`. - **Разработчик фронтенда** — [quickstart.md](quickstart.md) (раздел про `web/` и CORS), [api.md](api.md), [../web/README.md](../web/README.md). diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 8767277..e3b4186 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -556,10 +556,12 @@ components: properties: id: $ref: "#/components/schemas/ResourceId" - name: + community: type: string - kind: + description: Техническое значение BGP community (строка для BIRD, например 65001:120 или large JSON в value_json). + title: type: string + description: Человекочитаемое название для UI и фильтров. additionalProperties: true BgpPeer: @@ -768,20 +770,20 @@ components: BgpCommunityCreate: type: object - required: [name] + required: [community] properties: - name: + community: type: string - kind: + title: type: string additionalProperties: true BgpCommunityPatch: type: object properties: - name: + community: type: string - kind: + title: type: string additionalProperties: true diff --git a/internal/httpapi/routes_crud.go b/internal/httpapi/routes_crud.go index 2b60b63..775b9a8 100644 --- a/internal/httpapi/routes_crud.go +++ b/internal/httpapi/routes_crud.go @@ -576,7 +576,7 @@ func commJSON(x *store.Community) map[string]any { if err := json.Unmarshal([]byte(x.ValueJSON), &v); err != nil { v = x.ValueJSON } - return map[string]any{"id": x.ID, "name": x.Name, "kind": x.Kind, "value_json": v} + return map[string]any{"id": x.ID, "community": x.Community, "title": x.Title, "value_json": v} } func (s *Server) handleGetComm(w http.ResponseWriter, r *http.Request) { diff --git a/internal/pipeline/bird_materialize.go b/internal/pipeline/bird_materialize.go index acbca63..cebd228 100644 --- a/internal/pipeline/bird_materialize.go +++ b/internal/pipeline/bird_materialize.go @@ -90,5 +90,5 @@ func communityRouteBody(st store.Backend, tenantID string, cid *string) (string, if err != nil { return "", fmt.Errorf("community %s: %w", id, err) } - return birdfmt.RouteCommunityAttrs(c.Kind, c.Name, c.ValueJSON) + return birdfmt.RouteCommunityAttrs("", c.Community, c.ValueJSON) } diff --git a/internal/pipeline/refresh.go b/internal/pipeline/refresh.go index ce66a83..b55c9ec 100644 --- a/internal/pipeline/refresh.go +++ b/internal/pipeline/refresh.go @@ -33,6 +33,8 @@ func MaterializedASPrefixKey(asn int64) string { // RefreshModule runs ingest (where applicable) for one module, then renders a new revision whose // BIRD materialization includes prefixes from all enabled modules of the tenant (others via live collect). +// If the tenant-wide materialized prefix set is unchanged from the latest revision, returns that +// revision id and does not insert a duplicate config_revision. func RefreshModule(ctx context.Context, st store.Backend, hc *http.Client, tenantID, moduleID string) (revisionID string, err error) { if hc == nil { hc = http.DefaultClient @@ -50,13 +52,17 @@ func RefreshModule(ctx context.Context, st store.Backend, hc *http.Client, tenan return "", err } - revisionID = uuid.NewString() - parent := parentRevision(st, tenantID, moduleID) agg, err := aggregateTenantPrefixRows(ctx, st, hc, tenantID, moduleID, rows) if err != nil { return "", err } hash := hashAggregatedMaterialization(tenantID, agg) + if prev := latestTenantRevision(st, tenantID); prev != nil && prev.ContentHash == hash { + return prev.ID, nil + } + + revisionID = uuid.NewString() + parent := parentRevision(st, tenantID, moduleID) preview, err := buildPreviewFragments(st, tenantID, moduleID, revisionID, agg) if err != nil { return "", err @@ -233,6 +239,15 @@ func parentRevision(st store.Backend, tenantID, moduleID string) *string { return &id } +// latestTenantRevision is the newest config_revision for the tenant (any module), or nil. +func latestTenantRevision(st store.Backend, tenantID string) *store.Revision { + items, _, _ := st.ListRevisions(tenantID, "", "", 1) + if len(items) == 0 { + return nil + } + return items[0] +} + // hashAggregatedMaterialization hashes the full tenant-wide prefix set used for BIRD (all enabled modules). func hashAggregatedMaterialization(tenantID string, rows []store.PrefixRow) string { type line struct{ p, c, s string } diff --git a/internal/pipeline/refresh_aggregate_test.go b/internal/pipeline/refresh_aggregate_test.go index a059d19..58e8a16 100644 --- a/internal/pipeline/refresh_aggregate_test.go +++ b/internal/pipeline/refresh_aggregate_test.go @@ -25,27 +25,50 @@ func TestRefreshModule_AggregatesAllEnabledModules(t *testing.T) { } ctx := context.Background() - if _, err := RefreshModule(ctx, m, http.DefaultClient, tenant, modIP); err != nil { + before, _, _ := m.ListRevisions(tenant, "", "", 200) + beforeN := len(before) + + rev1, err := RefreshModule(ctx, m, http.DefaultClient, tenant, modIP) + if err != nil { t.Fatal(err) } - revs, _, _ := m.ListRevisions(tenant, modIP, "", 1) - if len(revs) == 0 { - t.Fatal("no revision") + after1, _, _ := m.ListRevisions(tenant, "", "", 200) + if len(after1) != beforeN+1 { + t.Fatalf("first refresh: want one new tenant revision, got %d -> %d", beforeN, len(after1)) } - px, _, _ := m.ListRevisionPrefixes(tenant, revs[0].ID, "", 1000) + px, _, _ := m.ListRevisionPrefixes(tenant, rev1, "", 1000) if len(px) != 2 { t.Fatalf("first refresh: want 2 aggregated prefixes, got %d: %+v", len(px), px) } - if _, err := RefreshModule(ctx, m, http.DefaultClient, tenant, mod2.ID); err != nil { + rev2, err := RefreshModule(ctx, m, http.DefaultClient, tenant, mod2.ID) + if err != nil { t.Fatal(err) } - revs2, _, _ := m.ListRevisions(tenant, mod2.ID, "", 1) - if len(revs2) == 0 { - t.Fatal("no revision for mod2") + if rev2 != rev1 { + t.Fatalf("second refresh: same materialization, want same revision id, got %s vs %s", rev2, rev1) } - px2, _, _ := m.ListRevisionPrefixes(tenant, revs2[0].ID, "", 1000) - if len(px2) != 2 { - t.Fatalf("second refresh: want 2 aggregated prefixes, got %d: %+v", len(px2), px2) + after2, _, _ := m.ListRevisions(tenant, "", "", 200) + if len(after2) != len(after1) { + t.Fatalf("second refresh: want no extra revision, had %d now %d", len(after1), len(after2)) + } + + if _, err := m.CreateIPRangeEntry(tenant, mod2.ID, &store.IPRangeEntry{Prefix: "10.0.1.0/24"}); err != nil { + t.Fatal(err) + } + rev3, err := RefreshModule(ctx, m, http.DefaultClient, tenant, mod2.ID) + if err != nil { + t.Fatal(err) + } + if rev3 == rev1 { + t.Fatal("after prefix change, expected a new revision") + } + after3, _, _ := m.ListRevisions(tenant, "", "", 200) + if len(after3) != len(after2)+1 { + t.Fatalf("third refresh: want one new revision, had %d now %d", len(after2), len(after3)) + } + px3, _, _ := m.ListRevisionPrefixes(tenant, rev3, "", 1000) + if len(px3) != 3 { + t.Fatalf("third refresh: want 3 prefixes, got %d", len(px3)) } } diff --git a/internal/repository/postgres.go b/internal/repository/postgres.go index 302c75e..3c1e31e 100644 --- a/internal/repository/postgres.go +++ b/internal/repository/postgres.go @@ -954,7 +954,7 @@ func (p *Postgres) DeleteDohProfile(tenantID, id string) error { func (p *Postgres) ListCommunities(tenantID string) ([]*store.Community, error) { ctx := context.Background() - rows, err := p.pool.Query(ctx, `SELECT id::text, name, kind, value_json::text FROM bgp_community WHERE tenant_id=$1`, tenantID) + rows, err := p.pool.Query(ctx, `SELECT id::text, community, title, value_json::text FROM bgp_community WHERE tenant_id=$1 ORDER BY COALESCE(NULLIF(trim(title), ''), community)`, tenantID) if err != nil { return nil, err } @@ -963,7 +963,7 @@ func (p *Postgres) ListCommunities(tenantID string) ([]*store.Community, error) for rows.Next() { var c store.Community c.TenantID = tenantID - if err := rows.Scan(&c.ID, &c.Name, &c.Kind, &c.ValueJSON); err != nil { + if err := rows.Scan(&c.ID, &c.Community, &c.Title, &c.ValueJSON); err != nil { continue } out = append(out, &c) @@ -975,8 +975,8 @@ func (p *Postgres) GetCommunity(tenantID, id string) (*store.Community, error) { ctx := context.Background() var c store.Community c.TenantID = tenantID - err := p.pool.QueryRow(ctx, `SELECT id::text, name, kind, value_json::text FROM bgp_community WHERE id=$1 AND tenant_id=$2`, id, tenantID).Scan( - &c.ID, &c.Name, &c.Kind, &c.ValueJSON) + err := p.pool.QueryRow(ctx, `SELECT id::text, community, title, value_json::text FROM bgp_community WHERE id=$1 AND tenant_id=$2`, id, tenantID).Scan( + &c.ID, &c.Community, &c.Title, &c.ValueJSON) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, store.ErrNotFound @@ -990,14 +990,19 @@ func (p *Postgres) CreateCommunity(tenantID string, in *store.Community) (*store if in == nil { return nil, store.ErrInvalidInput } + if strings.TrimSpace(in.Community) == "" { + return nil, store.ErrInvalidInput + } ctx := context.Background() id := uuid.NewString() vj := in.ValueJSON if strings.TrimSpace(vj) == "" { vj = "{}" } - _, err := p.pool.Exec(ctx, `INSERT INTO bgp_community (id, tenant_id, name, kind, value_json) VALUES ($1,$2,$3,$4,$5::jsonb)`, - id, tenantID, in.Name, in.Kind, vj) + comm := strings.TrimSpace(in.Community) + title := strings.TrimSpace(in.Title) + _, err := p.pool.Exec(ctx, `INSERT INTO bgp_community (id, tenant_id, community, title, value_json) VALUES ($1,$2,$3,$4,$5::jsonb)`, + id, tenantID, comm, title, vj) if err != nil { return nil, err } @@ -1009,18 +1014,21 @@ func (p *Postgres) UpdateCommunity(tenantID, id string, patch *store.CommunityPa if err != nil { return nil, err } - if patch.Name != nil { - cur.Name = *patch.Name + if patch.Community != nil { + cur.Community = strings.TrimSpace(*patch.Community) } - if patch.Kind != nil { - cur.Kind = *patch.Kind + if patch.Title != nil { + cur.Title = strings.TrimSpace(*patch.Title) } if patch.ValueJSON != nil { cur.ValueJSON = *patch.ValueJSON } + if strings.TrimSpace(cur.Community) == "" { + return nil, store.ErrInvalidInput + } ctx := context.Background() - _, err = p.pool.Exec(ctx, `UPDATE bgp_community SET name=$3, kind=$4, value_json=$5::jsonb, updated_at=now() WHERE id=$1 AND tenant_id=$2`, - id, tenantID, cur.Name, cur.Kind, cur.ValueJSON) + _, err = p.pool.Exec(ctx, `UPDATE bgp_community SET community=$3, title=$4, value_json=$5::jsonb, updated_at=now() WHERE id=$1 AND tenant_id=$2`, + id, tenantID, cur.Community, cur.Title, cur.ValueJSON) if err != nil { return nil, err } diff --git a/internal/repository/postgres_seed.go b/internal/repository/postgres_seed.go index d9af715..593412c 100644 --- a/internal/repository/postgres_seed.go +++ b/internal/repository/postgres_seed.go @@ -58,7 +58,7 @@ protocol direct { if _, err := tx.Exec(ctx, `INSERT INTO tenant (id, name, slug) VALUES ($1,'Demo','demo')`, tid); err != nil { return err } - if _, err := tx.Exec(ctx, `INSERT INTO bgp_community (id, tenant_id, name, kind, value_json) VALUES ($1,$2,'demo-comm','large','{}')`, cid, tid); err != nil { + if _, err := tx.Exec(ctx, `INSERT INTO bgp_community (id, tenant_id, community, title, value_json) VALUES ($1,$2,'demo-comm','Demo','{}')`, cid, tid); err != nil { return err } if _, err := tx.Exec(ctx, ` diff --git a/internal/store/backend.go b/internal/store/backend.go index 8409091..04d4136 100644 --- a/internal/store/backend.go +++ b/internal/store/backend.go @@ -173,14 +173,14 @@ type DohProfilePatch struct { type Community struct { ID string `json:"id,omitempty"` TenantID string `json:"tenant_id,omitempty"` - Name string `json:"name"` - Kind string `json:"kind"` + Community string `json:"community"` + Title string `json:"title"` ValueJSON string `json:"value_json"` } type CommunityPatch struct { - Name *string `json:"name,omitempty"` - Kind *string `json:"kind,omitempty"` + Community *string `json:"community,omitempty"` + Title *string `json:"title,omitempty"` ValueJSON *string `json:"value_json,omitempty"` } diff --git a/internal/store/memory.go b/internal/store/memory.go index 2f51c48..d14b692 100644 --- a/internal/store/memory.go +++ b/internal/store/memory.go @@ -235,7 +235,7 @@ protocol direct { // Demo materialized prefixes for /revisions/{id}/prefixes cid := uuid.NewString() - m.communities[cid] = &Community{ID: cid, TenantID: tid, Name: "demo-comm", Kind: "large", ValueJSON: "{}"} + m.communities[cid] = &Community{ID: cid, TenantID: tid, Community: "demo-comm", Title: "Demo", ValueJSON: "{}"} m.revPrefixes[rid] = []PrefixRow{ {Prefix: "203.0.113.0/24", CommunityID: &cid, Source: "demo"}, {Prefix: "2001:db8::/32", CommunityID: &cid, Source: "demo"}, diff --git a/internal/store/memory_crud.go b/internal/store/memory_crud.go index f643830..7a35b11 100644 --- a/internal/store/memory_crud.go +++ b/internal/store/memory_crud.go @@ -569,7 +569,7 @@ func (m *Memory) GetCommunity(tenantID, id string) (*Community, error) { } func (m *Memory) CreateCommunity(tenantID string, in *Community) (*Community, error) { - if in == nil || strings.TrimSpace(in.Kind) == "" { + if in == nil || strings.TrimSpace(in.Community) == "" { return nil, ErrInvalidInput } m.mu.Lock() @@ -582,7 +582,7 @@ func (m *Memory) CreateCommunity(tenantID string, in *Community) (*Community, er if strings.TrimSpace(vj) == "" { vj = "{}" } - c := &Community{ID: id, TenantID: tenantID, Name: in.Name, Kind: strings.TrimSpace(in.Kind), ValueJSON: vj} + c := &Community{ID: id, TenantID: tenantID, Community: strings.TrimSpace(in.Community), Title: strings.TrimSpace(in.Title), ValueJSON: vj} m.communities[id] = c return c, nil } @@ -597,11 +597,11 @@ func (m *Memory) UpdateCommunity(tenantID, id string, patch *CommunityPatch) (*C if !ok || c.TenantID != tenantID { return nil, ErrNotFound } - if patch.Name != nil { - c.Name = *patch.Name + if patch.Community != nil { + c.Community = strings.TrimSpace(*patch.Community) } - if patch.Kind != nil { - c.Kind = strings.TrimSpace(*patch.Kind) + if patch.Title != nil { + c.Title = strings.TrimSpace(*patch.Title) } if patch.ValueJSON != nil { c.ValueJSON = *patch.ValueJSON diff --git a/migrations/postgres/000005_bgp_community_title.down.sql b/migrations/postgres/000005_bgp_community_title.down.sql new file mode 100644 index 0000000..c6318fc --- /dev/null +++ b/migrations/postgres/000005_bgp_community_title.down.sql @@ -0,0 +1,9 @@ +ALTER TABLE bgp_community RENAME COLUMN community TO name; + +ALTER TABLE bgp_community ADD COLUMN kind TEXT NOT NULL DEFAULT 'large'; + +UPDATE bgp_community SET kind = 'large' WHERE trim(kind) = ''; + +ALTER TABLE bgp_community DROP COLUMN title; + +ALTER TABLE bgp_community ADD CONSTRAINT bgp_community_kind_chk CHECK (length(trim(kind)) > 0); diff --git a/migrations/postgres/000005_bgp_community_title.up.sql b/migrations/postgres/000005_bgp_community_title.up.sql new file mode 100644 index 0000000..b3117f9 --- /dev/null +++ b/migrations/postgres/000005_bgp_community_title.up.sql @@ -0,0 +1,10 @@ +-- Техническое значение BGP в колонке community; человекочитаемое имя в title; поле kind удалено. +ALTER TABLE bgp_community ADD COLUMN title TEXT NOT NULL DEFAULT ''; + +UPDATE bgp_community SET title = trim(name) WHERE title = ''; + +ALTER TABLE bgp_community DROP CONSTRAINT bgp_community_kind_chk; + +ALTER TABLE bgp_community DROP COLUMN kind; + +ALTER TABLE bgp_community RENAME COLUMN name TO community; diff --git a/migrations/sqlite/000005_bgp_community_title.down.sql b/migrations/sqlite/000005_bgp_community_title.down.sql new file mode 100644 index 0000000..650f56f --- /dev/null +++ b/migrations/sqlite/000005_bgp_community_title.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE bgp_community RENAME COLUMN community TO name; + +ALTER TABLE bgp_community ADD COLUMN kind TEXT NOT NULL DEFAULT 'large'; + +ALTER TABLE bgp_community DROP COLUMN title; diff --git a/migrations/sqlite/000005_bgp_community_title.up.sql b/migrations/sqlite/000005_bgp_community_title.up.sql new file mode 100644 index 0000000..b9a2884 --- /dev/null +++ b/migrations/sqlite/000005_bgp_community_title.up.sql @@ -0,0 +1,8 @@ +-- Параллельно PostgreSQL: community + title, без kind (SQLite 3.35+). +ALTER TABLE bgp_community ADD COLUMN title TEXT NOT NULL DEFAULT ''; + +UPDATE bgp_community SET title = trim(name) WHERE title = ''; + +ALTER TABLE bgp_community DROP COLUMN kind; + +ALTER TABLE bgp_community RENAME COLUMN name TO community; diff --git a/web/src/lib/api/types.ts b/web/src/lib/api/types.ts index 5d29184..76c6ed7 100644 --- a/web/src/lib/api/types.ts +++ b/web/src/lib/api/types.ts @@ -113,12 +113,12 @@ export type DohProfilesResponse = Page; // ---- Communities ---- export type BgpCommunity = { id: string; - name: string; - kind: string | null; + community: string; + title: string; }; export type BgpCommunityCreate = { - name: string; - kind?: string; + community: string; + title?: string; }; export type BgpCommunityPatch = Partial; export type CommunitiesResponse = Page; diff --git a/web/src/routes/directories/+page.svelte b/web/src/routes/directories/+page.svelte index 3d0779a..56f4b90 100644 --- a/web/src/routes/directories/+page.svelte +++ b/web/src/routes/directories/+page.svelte @@ -9,7 +9,6 @@ DohProfileCreate, DohProfilesResponse } from '$lib/api/types.js'; - import { Badge } from '$lib/components/ui/badge/index.js'; import { Button } from '$lib/components/ui/button/index.js'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '$lib/components/ui/card/index.js'; import { Input } from '$lib/components/ui/input/index.js'; @@ -52,7 +51,7 @@ let commLoading = $state(false); let commDialog = $state(false); let commEdit = $state(null); - let commForm = $state({ name: '', kind: '' }); + let commForm = $state({ community: '', title: '' }); let commSaving = $state(false); let commDeleteTarget = $state(null); @@ -92,21 +91,27 @@ onMount(() => { loadComm(); loadDoh(); }); // --- Community actions --- + function commDisplay(c: BgpCommunity | null) { + if (!c) return ''; + const t = c.title?.trim(); + return t || c.community; + } + function openCommCreate() { commEdit = null; - commForm = { name: '', kind: '' }; + commForm = { community: '', title: '' }; commDialog = true; } function openCommEdit(c: BgpCommunity) { commEdit = c; - commForm = { name: c.name, kind: c.kind ?? '' }; + commForm = { community: c.community, title: c.title ?? '' }; commDialog = true; } async function saveComm() { - if (!commForm.name.trim()) { toast.error('Укажите название'); return; } + if (!commForm.community.trim()) { toast.error('Укажите community'); return; } commSaving = true; try { - const body = { ...commForm, kind: commForm.kind || undefined }; + const body = { ...commForm, title: commForm.title?.trim() || undefined }; if (commEdit) { await apiMutate(`/v1/communities/${commEdit.id}`, 'PATCH', body); toast.success('Community обновлена'); @@ -208,8 +213,8 @@ + Community Название - Вид ID @@ -217,14 +222,8 @@ {#each communities as c (c.id)} - {c.name} - - {#if c.kind} - {c.kind} - {:else} - - {/if} - + {c.community} + {c.title?.trim() || '—'} {c.id}
@@ -307,12 +306,12 @@
- - + +
- - + +
@@ -325,7 +324,7 @@ { if (!v) commDeleteTarget = null; }}> - Удалить community «{commDeleteTarget?.name}»? + Удалить community «{commDisplay(commDeleteTarget)}»? Это приведёт к удалению привязки во всех модулях. diff --git a/web/src/routes/modules/[moduleId]/+page.svelte b/web/src/routes/modules/[moduleId]/+page.svelte index 0cb6c60..91be897 100644 --- a/web/src/routes/modules/[moduleId]/+page.svelte +++ b/web/src/routes/modules/[moduleId]/+page.svelte @@ -390,9 +390,17 @@ } } - function communityName(id: string | null) { + function communityLabel(id: string | null) { if (!id) return '—'; - return communities.find((c) => c.id === id)?.name ?? id.slice(0, 8) + '…'; + const c = communities.find((x) => x.id === id); + if (!c) return id.slice(0, 8) + '…'; + const t = c.title?.trim(); + return t || c.community; + } + + function communityOptionLabel(c: BgpCommunity) { + const t = c.title?.trim(); + return t || c.community; } const activeTab = $derived.by(() => { @@ -464,7 +472,7 @@

Community по умолч.

-

{communityName(mod.default_community_id)}

+

{communityLabel(mod.default_community_id)}

@@ -510,7 +518,7 @@ ? new Date(entry.asn_resolved_at).toLocaleString('ru-RU') : '—'}
- {communityName(entry.community_id)} + {communityLabel(entry.community_id)}
@@ -552,7 +560,7 @@ {src.url} {src.source_kind} - {communityName(src.community_id)} + {communityLabel(src.community_id)} {src.refresh_interval_sec ? `${src.refresh_interval_sec}с` : '—'}
@@ -592,7 +600,7 @@ {#each domainEntries as entry (entry.id)} {entry.fqdn} - {communityName(entry.community_id)} + {communityLabel(entry.community_id)}
@@ -631,7 +639,7 @@ {#each ipEntries as entry (entry.id)} {entry.prefix} - {communityName(entry.community_id)} + {communityLabel(entry.community_id)}
@@ -683,12 +691,12 @@ @@ -765,12 +773,12 @@ @@ -815,12 +823,12 @@ @@ -865,12 +873,12 @@ @@ -911,11 +919,11 @@