CI / changes (push) Successful in 17s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 26s
CI / web (push) Successful in 46s
CI / go (push) Successful in 1m1s
CI / bird2 (push) Successful in 17s
CI / release (push) Failing after 2m22s
Web UI полностью переведён с SvelteKit на новый стек: React 19, TanStack Router/Query/Table/Virtual, shadcn/ui (base-nova) и ReUI enterprise-компоненты (data-grid, filters, autocomplete). Новый код разложен по слоям: packages/ui (shadcn-примитивы), apps/web (роуты, shared-обёртки, ReUI-адаптации). BREAKING CHANGE: меняется структура и инструментинг фронтенда. - apps/web/ — новый Vite + React-проект (@evobgp/web), file-based роуты TanStack Router; экраны dashboard, modules, monitoring, network, operations, schedule, settings, tenant-settings, access, directories. - packages/ui/ — shadcn/ui-примитивы (@evobgp/ui) с общими стилями globals.css и cn-утилитой; CLI shadcn запускается из apps/web. - apps/web/src/components/reui/ — enterprise-паттерны ReUI. - pnpm workspace (pnpm-workspace.yaml, pnpm-lock.yaml, tsconfig.base.json) заменяет npm-проект в web/. - web/ переименован в web-legacy-svelte/ (архив-референс для миграции); импорты оттуда запрещены правилом WEB-22. - CI (.gitea/workflows/ci.yaml): job web переведён на Node 22 + pnpm 10 (typecheck/lint/build через pnpm --filter @evobgp/web); пути триггеров обновлены под apps/web|packages/ui. - deploy/docker/evobgp-web/Dockerfile: сборка из корня репозитория, pnpm install --frozen-lockfile, выход dist из apps/web/dist. - .cursor/rules/web-shadcn.mdc, context7-stack.mdc, engineering.mdc, AGENTS.md — обновлены под React-стек (WEB-01..WEB-22, DOC-SYNC-06/07). Проверки WEB-19 локально: typecheck, lint, build — exit 0. Co-authored-by: Cursor <[email protected]>
116 lines
3.2 KiB
TypeScript
116 lines
3.2 KiB
TypeScript
import { queryOptions } from '@tanstack/react-query'
|
|
import { apiJSON } from '@/lib/api-client'
|
|
import type {
|
|
JobRow,
|
|
JobsResponse,
|
|
ModuleRow,
|
|
ModulesResponse,
|
|
PeerRow,
|
|
PeersResponse,
|
|
RevisionRow,
|
|
RevisionsResponse,
|
|
SpeakerRow,
|
|
SpeakersResponse,
|
|
} from '@/types/api'
|
|
|
|
export const overviewKeys = {
|
|
all: ['overview'] as const,
|
|
modules: () => [...overviewKeys.all, 'modules'] as const,
|
|
peers: () => [...overviewKeys.all, 'peers'] as const,
|
|
speakers: () => [...overviewKeys.all, 'speakers'] as const,
|
|
revisions: () => [...overviewKeys.all, 'revisions'] as const,
|
|
jobs: () => [...overviewKeys.all, 'jobs'] as const,
|
|
health: () => [...overviewKeys.all, 'health'] as const,
|
|
}
|
|
|
|
export function overviewModulesQueryOptions() {
|
|
return queryOptions<ModulesResponse>({
|
|
queryKey: overviewKeys.modules(),
|
|
queryFn: () => apiJSON<ModulesResponse>('/v1/modules?limit=200'),
|
|
staleTime: 60_000,
|
|
})
|
|
}
|
|
|
|
export function overviewPeersQueryOptions() {
|
|
return queryOptions<PeersResponse>({
|
|
queryKey: overviewKeys.peers(),
|
|
queryFn: () => apiJSON<PeersResponse>('/v1/peers?limit=200&live=1'),
|
|
staleTime: 30_000,
|
|
})
|
|
}
|
|
|
|
export function overviewSpeakersQueryOptions() {
|
|
return queryOptions<SpeakersResponse>({
|
|
queryKey: overviewKeys.speakers(),
|
|
queryFn: () => apiJSON<SpeakersResponse>('/v1/speakers?limit=200&live=1'),
|
|
staleTime: 30_000,
|
|
})
|
|
}
|
|
|
|
export function overviewRevisionsQueryOptions() {
|
|
return queryOptions<RevisionsResponse>({
|
|
queryKey: overviewKeys.revisions(),
|
|
queryFn: () => apiJSON<RevisionsResponse>('/v1/revisions?limit=10'),
|
|
staleTime: 60_000,
|
|
})
|
|
}
|
|
|
|
export function overviewJobsQueryOptions() {
|
|
return queryOptions<JobsResponse>({
|
|
queryKey: overviewKeys.jobs(),
|
|
queryFn: () => apiJSON<JobsResponse>('/v1/jobs?limit=10'),
|
|
staleTime: 15_000,
|
|
})
|
|
}
|
|
|
|
export function overviewHealthQueryOptions() {
|
|
return queryOptions<boolean>({
|
|
queryKey: overviewKeys.health(),
|
|
queryFn: async () => {
|
|
const res = await fetch('/v1/health')
|
|
return res.ok
|
|
},
|
|
staleTime: 30_000,
|
|
})
|
|
}
|
|
|
|
// Selectors / helpers
|
|
export type NetworkMetrics = {
|
|
peersTotal: number
|
|
peersEnabled: number
|
|
peersEstablished: number
|
|
peersMismatch: number
|
|
speakersTotal: number
|
|
speakersOnline: number
|
|
}
|
|
|
|
export function aggregateNetworkMetrics(
|
|
peers: PeerRow[],
|
|
speakers: SpeakerRow[],
|
|
): NetworkMetrics {
|
|
const peersEnabled = peers.filter((p) => p.enabled !== false).length
|
|
const peersEstablished = peers.filter((p) => p.session_state === 'Established').length
|
|
const peersMismatch = peers.filter((p) => p.session_mismatch).length
|
|
const speakersOnline = speakers.filter((s) => s.live?.agent_ok).length
|
|
return {
|
|
peersTotal: peers.length,
|
|
peersEnabled,
|
|
peersEstablished,
|
|
peersMismatch,
|
|
speakersTotal: speakers.length,
|
|
speakersOnline,
|
|
}
|
|
}
|
|
|
|
export function runningJobCount(jobs: JobRow[]): number {
|
|
return jobs.filter((j) => j.status === 'running' || j.status === 'queued').length
|
|
}
|
|
|
|
export function moduleNameById(modules: ModuleRow[]): Map<string, string> {
|
|
return new Map(modules.map((m) => [m.id, m.name]))
|
|
}
|
|
|
|
export function recentRevisions(revisions: RevisionRow[], n = 10): RevisionRow[] {
|
|
return revisions.slice(0, n)
|
|
}
|