feat: enhance evobgp with new command-line tools for bundle management, including pull, verify, and apply functionalities. Update go.mod to include necessary dependencies and complete todos in architecture plan for improved observability and deployment practices.
CI / changes (push) Successful in 4s
CI / go (push) Failing after 6s
CI / bird2 (push) Has been skipped
CI / openapi (push) Has been skipped

This commit is contained in:
Denozordec
2026-04-05 14:07:45 +07:00
parent 272542b92a
commit bf52b21150
131 changed files with 9222 additions and 17 deletions
+60
View File
@@ -0,0 +1,60 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { page } from '$app/state';
import { buttonVariants } from '$lib/components/ui/button/button.svelte';
import { cn } from '$lib/utils.js';
import { Separator } from '$lib/components/ui/separator/index.js';
import Activity from '@lucide/svelte/icons/activity';
import Boxes from '@lucide/svelte/icons/boxes';
import CalendarClock from '@lucide/svelte/icons/calendar-clock';
import LayoutDashboard from '@lucide/svelte/icons/layout-dashboard';
import Network from '@lucide/svelte/icons/network';
import Settings from '@lucide/svelte/icons/settings';
import Gauge from '@lucide/svelte/icons/gauge';
const nav = [
{ href: '/', label: 'Обзор', icon: LayoutDashboard },
{ href: '/modules', label: 'Модули', icon: Boxes },
{ href: '/revisions', label: 'Ревизии', icon: Activity },
{ href: '/peers', label: 'Пиры и спикеры', icon: Network },
{ href: '/schedule', label: 'Расписание', icon: CalendarClock },
{ href: '/monitoring', label: 'Мониторинг', icon: Gauge },
{ href: '/settings', label: 'Настройки', icon: Settings }
] as const;
let { children } = $props();
</script>
<div class="bg-background flex min-h-screen">
<aside
class="border-border bg-card/30 flex w-56 shrink-0 flex-col border-r"
aria-label="Основная навигация"
>
<div class="p-4">
<a href={resolve('/')} class="text-foreground font-semibold tracking-tight">EvoBGP</a>
<p class="text-muted-foreground mt-0.5 text-xs">Control plane</p>
</div>
<Separator />
<nav class="flex flex-1 flex-col gap-0.5 p-2">
{#each nav as item (item.href)}
{@const Icon = item.icon}
<a
href={resolve(item.href)}
class={cn(
buttonVariants({
variant: page.url.pathname === item.href ? 'secondary' : 'ghost',
size: 'sm'
}),
'w-full justify-start gap-2 no-underline'
)}
>
<Icon class="size-4" />
{item.label}
</a>
{/each}
</nav>
</aside>
<main class="min-w-0 flex-1 p-6">
{@render children?.()}
</main>
</div>
+47
View File
@@ -0,0 +1,47 @@
import { browser } from '$app/environment';
export const TOKEN_STORAGE_KEY = 'evobgp_api_token';
export type Problem = {
title?: string;
status?: number;
detail?: string;
};
function mergeHeaders(init?: RequestInit): Headers {
const h = new Headers(init?.headers);
if (!h.has('Accept')) {
h.set('Accept', 'application/json');
}
if (browser) {
const t = localStorage.getItem(TOKEN_STORAGE_KEY);
if (t && !h.has('Authorization')) {
h.set('Authorization', `Bearer ${t}`);
}
}
return h;
}
export async function apiFetch(path: string, init?: RequestInit): Promise<Response> {
if (!browser) {
throw new Error('API is only available in the browser');
}
return fetch(path, { ...init, headers: mergeHeaders(init) });
}
export async function apiJSON<T>(path: string, init?: RequestInit): Promise<T> {
const res = await apiFetch(path, init);
const text = await res.text();
if (!res.ok) {
let detail = text;
try {
const j = JSON.parse(text) as Problem;
if (j?.detail) detail = j.detail;
} catch {
/* plain text */
}
throw new Error(detail || `HTTP ${res.status}`);
}
if (!text) return undefined as T;
return JSON.parse(text) as T;
}
+77
View File
@@ -0,0 +1,77 @@
export type ModuleRow = {
id: string;
type: string;
name: string;
enabled: boolean;
priority: number;
refresh_interval_sec: number;
cron_expr: string;
default_community_id: string | null;
doh_profile_id: string | null;
};
export type ModulesResponse = {
items: ModuleRow[];
next_cursor: string | null;
has_more: boolean;
};
export type RevisionRow = {
id: string;
content_hash: string;
created_at: string;
parent_revision_id: string | null;
materialized_prefix_count: number;
module_id: string | null;
};
export type RevisionsResponse = {
items: RevisionRow[];
next_cursor: string | null;
has_more: boolean;
};
export type PeerRow = {
id: string;
name: string;
neighbor: string;
session_state: string;
bgp_speaker_id: string | null;
};
export type PeersResponse = {
items: PeerRow[];
next_cursor: string | null;
has_more: boolean;
};
export type SpeakerRow = {
id: string;
role: string;
endpoint: string;
last_applied_revision_id: string | null;
};
export type SpeakersResponse = {
items: SpeakerRow[];
next_cursor: string | null;
has_more: boolean;
};
export type JobRow = {
job_id: string;
kind: string;
status: string;
idempotency_key?: string | null;
created_at?: string;
started_at?: string | null;
finished_at?: string | null;
error?: string | null;
meta?: Record<string, unknown>;
};
export type JobsResponse = {
items: JobRow[];
next_cursor: string | null;
has_more: boolean;
};
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,38 @@
<script lang="ts" module>
import { type VariantProps, tv } from 'tailwind-variants';
export const badgeVariants = tv({
base: 'focus:ring-ring inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:ring-2 focus:ring-offset-2 focus:outline-none',
variants: {
variant: {
default: 'bg-primary text-primary-foreground border-transparent shadow hover:bg-primary/80',
secondary: 'bg-secondary text-secondary-foreground border-transparent hover:bg-secondary/80',
destructive:
'bg-destructive text-destructive-foreground border-transparent shadow hover:bg-destructive/80',
outline: 'text-foreground'
}
},
defaultVariants: {
variant: 'default'
}
});
export type BadgeVariant = VariantProps<typeof badgeVariants>['variant'];
</script>
<script lang="ts">
import { cn } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
class: className,
variant = 'default',
children,
...rest
}: HTMLAttributes<HTMLSpanElement> & {
variant?: BadgeVariant;
children?: import('svelte').Snippet;
} = $props();
</script>
<span class={cn(badgeVariants({ variant }), className)} {...rest}>{@render children?.()}</span>
+2
View File
@@ -0,0 +1,2 @@
import Root, { badgeVariants, type BadgeVariant } from './badge.svelte';
export { Root, Root as Badge, badgeVariants, type BadgeVariant };
@@ -0,0 +1,54 @@
<script lang="ts" module>
import { type VariantProps, tv } from 'tailwind-variants';
export const buttonVariants = tv({
base: 'focus-visible:border-ring focus-visible:ring-ring/50 inline-flex shrink-0 cursor-pointer select-none items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*="size-"])]:size-4',
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
destructive:
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:hover:bg-input/50',
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
});
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
</script>
<script lang="ts">
import { cn } from '$lib/utils.js';
import type { HTMLButtonAttributes } from 'svelte/elements';
let {
class: className,
variant = 'default',
size = 'default',
type = 'button',
children,
...rest
}: HTMLButtonAttributes & {
variant?: ButtonVariant;
size?: ButtonSize;
children?: import('svelte').Snippet;
} = $props();
</script>
<button class={cn(buttonVariants({ variant, size }), className)} {type} {...rest}
>{@render children?.()}</button
>
@@ -0,0 +1,8 @@
import Root, { buttonVariants, type ButtonSize, type ButtonVariant } from './button.svelte';
export {
Root,
Root as Button,
buttonVariants,
type ButtonSize,
type ButtonVariant
};
@@ -0,0 +1,12 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
class: className,
children,
...rest
}: HTMLAttributes<HTMLDivElement> & { children?: import('svelte').Snippet } = $props();
</script>
<div class={cn('p-6 pt-0', className)} {...rest}>{@render children?.()}</div>
@@ -0,0 +1,12 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
class: className,
children,
...rest
}: HTMLAttributes<HTMLParagraphElement> & { children?: import('svelte').Snippet } = $props();
</script>
<p class={cn('text-muted-foreground text-sm', className)} {...rest}>{@render children?.()}</p>
@@ -0,0 +1,12 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
class: className,
children,
...rest
}: HTMLAttributes<HTMLDivElement> & { children?: import('svelte').Snippet } = $props();
</script>
<div class={cn('flex flex-col gap-1.5 p-6', className)} {...rest}>{@render children?.()}</div>
@@ -0,0 +1,14 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
class: className,
children,
...rest
}: HTMLAttributes<HTMLHeadingElement> & { children?: import('svelte').Snippet } = $props();
</script>
<h3 class={cn('leading-none font-semibold tracking-tight', className)} {...rest}>
{@render children?.()}
</h3>
@@ -0,0 +1,17 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
class: className,
children,
...rest
}: HTMLAttributes<HTMLDivElement> & { children?: import('svelte').Snippet } = $props();
</script>
<div
class={cn('bg-card text-card-foreground rounded-xl border shadow-sm', className)}
{...rest}
>
{@render children?.()}
</div>
+14
View File
@@ -0,0 +1,14 @@
import Root from './card.svelte';
import Header from './card-header.svelte';
import Title from './card-title.svelte';
import Description from './card-description.svelte';
import Content from './card-content.svelte';
export {
Root,
Root as Card,
Header as CardHeader,
Title as CardTitle,
Description as CardDescription,
Content as CardContent
};
+2
View File
@@ -0,0 +1,2 @@
import Root from './input.svelte';
export { Root, Root as Input };
@@ -0,0 +1,19 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import type { HTMLInputAttributes } from 'svelte/elements';
let {
class: className,
value = $bindable(),
...rest
}: HTMLInputAttributes = $props();
</script>
<input
class={cn(
'border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border px-3 py-1 text-base shadow-xs transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-[3px] focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className
)}
bind:value
{...rest}
/>
+2
View File
@@ -0,0 +1,2 @@
import Root from './label.svelte';
export { Root, Root as Label };
@@ -0,0 +1,15 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import type { HTMLLabelAttributes } from 'svelte/elements';
let {
class: className,
children,
...rest
}: HTMLLabelAttributes & { children?: import('svelte').Snippet } = $props();
</script>
<label
class={cn('text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70', className)}
{...rest}>{@render children?.()}</label
>
@@ -0,0 +1,2 @@
import Root from './scroll-area.svelte';
export { Root, Root as ScrollArea };
@@ -0,0 +1,12 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
class: className,
children,
...rest
}: HTMLAttributes<HTMLDivElement> & { children?: import('svelte').Snippet } = $props();
</script>
<div class={cn('relative overflow-auto', className)} {...rest}>{@render children?.()}</div>
@@ -0,0 +1,2 @@
import Root from './separator.svelte';
export { Root, Root as Separator };
@@ -0,0 +1,20 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
class: className,
orientation = 'horizontal',
...rest
}: HTMLAttributes<HTMLDivElement> & { orientation?: 'horizontal' | 'vertical' } = $props();
</script>
<div
role="separator"
class={cn(
'bg-border shrink-0',
orientation === 'horizontal' ? 'h-px w-full' : 'h-full w-px',
className
)}
{...rest}
></div>
+16
View File
@@ -0,0 +1,16 @@
import Root from './table.svelte';
import Header from './table-header.svelte';
import Body from './table-body.svelte';
import Row from './table-row.svelte';
import Head from './table-head.svelte';
import Cell from './table-cell.svelte';
export {
Root,
Root as Table,
Header as TableHeader,
Body as TableBody,
Row as TableRow,
Head as TableHead,
Cell as TableCell
};
@@ -0,0 +1,12 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
class: className,
children,
...rest
}: HTMLAttributes<HTMLTableSectionElement> & { children?: import('svelte').Snippet } = $props();
</script>
<tbody class={cn('[&_tr:last-child]:border-0', className)} {...rest}>{@render children?.()}</tbody>
@@ -0,0 +1,23 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
class: className,
colspan,
rowspan,
children,
...rest
}: HTMLAttributes<HTMLTableCellElement> & {
colspan?: number;
rowspan?: number;
children?: import('svelte').Snippet;
} = $props();
</script>
<td
class={cn('p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]', className)}
{colspan}
{rowspan}
{...rest}>{@render children?.()}</td
>
@@ -0,0 +1,18 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
class: className,
children,
...rest
}: HTMLAttributes<HTMLTableCellElement> & { children?: import('svelte').Snippet } = $props();
</script>
<th
class={cn(
'text-muted-foreground h-10 px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className
)}
{...rest}>{@render children?.()}</th
>
@@ -0,0 +1,12 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
class: className,
children,
...rest
}: HTMLAttributes<HTMLTableSectionElement> & { children?: import('svelte').Snippet } = $props();
</script>
<thead class={cn('[&_tr]:border-b', className)} {...rest}>{@render children?.()}</thead>
@@ -0,0 +1,15 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
class: className,
children,
...rest
}: HTMLAttributes<HTMLTableRowElement> & { children?: import('svelte').Snippet } = $props();
</script>
<tr
class={cn('hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors', className)}
{...rest}>{@render children?.()}</tr
>
@@ -0,0 +1,14 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
class: className,
children,
...rest
}: HTMLAttributes<HTMLTableElement> & { children?: import('svelte').Snippet } = $props();
</script>
<div class="relative w-full overflow-auto">
<table class={cn('w-full caption-bottom text-sm', className)} {...rest}>{@render children?.()}</table>
</div>
+1
View File
@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.
+13
View File
@@ -0,0 +1,13 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, 'child'> : T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, 'children'> : T;
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };