feat(api): enhance peer session management and documentation
CI / changes (push) Successful in 8s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 27s
CI / web (push) Failing after 35s
CI / go (push) Successful in 55s
CI / bird2 (push) Successful in 15s
CI / release (push) Has been skipped

- Added new fields to the API for tracking connected speakers and session states across multiple nodes, including `connected_speaker_id`, `connected_speaker_label`, `session_on_speakers`, `established_on_speakers`, and `session_mismatch`.
- Implemented a new endpoint for retrieving bird protocol sessions, enhancing the agent server functionality.
- Updated the OpenAPI documentation to reflect the new fields and query parameters, improving clarity for API consumers.
- Modified the frontend to display connected speaker information and session states, providing better visibility into peer connections.
- Updated deployment documentation to clarify the configuration requirements for enabling IP forwarding on VPS.
This commit is contained in:
Denozordec
2026-05-21 15:18:21 +07:00
parent e0a912a693
commit 1c0d78b552
16 changed files with 659 additions and 38 deletions
+11
View File
@@ -152,6 +152,12 @@ export type BgpCommunityPatch = Partial<BgpCommunityCreate>;
export type CommunitiesResponse = Page<BgpCommunity>;
// ---- Peers ----
export type PeerSessionOnSpeaker = {
speaker_id: string;
label: string;
state: string;
};
export type PeerRow = {
id: string;
name?: string;
@@ -160,6 +166,11 @@ export type PeerRow = {
enabled?: boolean;
session_state: string;
bgp_speaker_id: string | null;
connected_speaker_id?: string | null;
connected_speaker_label?: string;
session_on_speakers?: PeerSessionOnSpeaker[];
established_on_speakers?: PeerSessionOnSpeaker[];
session_mismatch?: boolean;
};
export type PeersResponse = Page<PeerRow>;
export type BgpPeerCreate = {
@@ -77,12 +77,47 @@
{ id: 'actions', label: '', class: 'w-20' }
] as const;
function speakerLabelById(id: string | null | undefined) {
if (!id) return '—';
return speakerById.get(id)?.endpoint ?? id;
function peerConnectedLabel(p: PeerRow): string {
const established = p.established_on_speakers ?? [];
if (established.length > 0) {
const labels = established.map((s) => s.label).join(', ');
return established.length === 1
? `Подключён: ${labels}`
: `Подключён (${established.length} нод): ${labels}`;
}
if (p.connected_speaker_label?.trim()) {
return `На ноде: ${p.connected_speaker_label.trim()}`;
}
if (p.session_on_speakers && p.session_on_speakers.length > 0) {
const parts = p.session_on_speakers.map((s) => `${s.label} (${s.state})`);
return parts.join(', ');
}
return 'Не найден на опрошенных нодах';
}
function sessionBadge(state: string) {
function peerSessionHint(p: PeerRow): string | null {
if (!p.session_mismatch || !p.bgp_speaker_id) return null;
const expected = speakerLabelById(p.bgp_speaker_id);
const actual =
p.established_on_speakers?.map((s) => s.label).join(', ') ||
p.connected_speaker_label?.trim() ||
'другие ноды';
return `В конфиге: ${expected}; Established на: ${actual}`;
}
function speakerLabelById(id: string | null | undefined) {
if (!id) return 'Все спикеры';
const s = speakerById.get(id);
if (!s) return id.slice(0, 8) + '…';
if (s.role === 'master') {
const host = s.agent_domain ?? s.endpoint;
return host ? `CP · ${host}` : 'CP (master)';
}
return s.agent_domain ?? s.endpoint ?? id.slice(0, 8) + '…';
}
function sessionBadge(state: string, p: PeerRow): 'default' | 'secondary' | 'destructive' | 'outline' {
if (p.session_mismatch) return 'destructive';
if (state === 'Established') return 'default';
if (state === 'Active' || state === 'Connect') return 'secondary';
return 'outline';
@@ -203,9 +238,22 @@
/>
</div>
{:else if column.id === 'session_state'}
<Badge variant={sessionBadge(p.session_state)}>{p.session_state || '—'}</Badge>
<div class="flex min-w-0 flex-col gap-0.5">
<Badge variant={sessionBadge(p.session_state, p)}>{p.session_state || '—'}</Badge>
<span
class="truncate text-xs text-muted-foreground"
title={peerSessionHint(p) ?? peerConnectedLabel(p)}
>
{peerConnectedLabel(p)}
</span>
{#if peerSessionHint(p)}
<span class="truncate text-xs text-destructive">{peerSessionHint(p)}</span>
{/if}
</div>
{:else if column.id === 'speaker'}
<span class="text-xs text-muted-foreground">{speakerLabelById(p.bgp_speaker_id)}</span>
<span class="text-xs text-muted-foreground" title="Привязка в конфиге CP">
{p.bgp_speaker_id ? speakerLabelById(p.bgp_speaker_id) : 'Все спикеры'}
</span>
{:else if column.id === 'actions'}
<div class="flex gap-1">
<Button variant="ghost" size="icon-sm" onclick={() => openEdit(p)}>
@@ -251,7 +299,7 @@
}}
>
<SelectTrigger id="p-speaker" class="w-full">
{form.bgp_speaker_id ? speakerLabelById(form.bgp_speaker_id) : 'Не выбрано'}
{form.bgp_speaker_id ? speakerLabelById(form.bgp_speaker_id) : 'Все спикеры'}
</SelectTrigger>
<SelectContent>
<SelectItem value="">Не выбрано</SelectItem>
+1 -1
View File
@@ -98,7 +98,7 @@
async function loadPeers() {
peersLoading = true;
try {
const pr = await apiJSON<PeersResponse>('/v1/peers?limit=200');
const pr = await apiJSON<PeersResponse>('/v1/peers?limit=200&live=1');
peers = pr.items;
} catch (e) {
loadError = e instanceof Error ? e.message : String(e);